From b5664c3d20f3620bda191db35ff7d47a6ec16c5e Mon Sep 17 00:00:00 2001 From: Carlo Costino Date: Fri, 28 Jul 2023 09:31:45 -0400 Subject: [PATCH] Add support for E2E (end-to-end) tests (#625) This changeset lays the foundation for supporting E2E (end-to-end) integration tests for US Notify. It brings in the Playwright testing framework along with the Playwright pytest plugin to make this possible, and includes the following adjustments: - A new test session fixture for ensuring that Playwright authenticates with the sites that are currently behind HTTP Auth (requies env-var config) - A new end_to_end test directory specifically for E2E tests - Updates to the Makefile that make sure E2E tests are not run as a part of the normal test routine but can be run separately - A new command in the Makefile to run E2E tests that will run in Chromium, Firefox, and Webkit headless browsers Signed-off-by: Carlo Costino --- .github/workflows/checks.yml | 8 +- Makefile | 8 +- Pipfile | 1 + Pipfile.lock | 127 +++++++++++++++--- README.md | 21 ++- docs/end_to_end_tests.md | 120 +++++++++++++++++ tests/conftest.py | 12 ++ .../test_landing_and_sign_in_pages.py | 109 +++++++++++++++ 8 files changed, 385 insertions(+), 21 deletions(-) create mode 100644 docs/end_to_end_tests.md create mode 100644 tests/end_to_end/test_landing_and_sign_in_pages.py diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index cb1062e3c..1dc92d63f 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -41,7 +41,13 @@ jobs: - name: Run js tests run: npm test - name: Run py tests with coverage - run: pipenv run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 + run: pipenv run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 --ignore=tests/end_to_end tests/ + - name: Run E2E tests + run: pipenv run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end + env: + NOTIFY_STAGING_HTTP_AUTH_PASSWORD: ${{ secrets.NOTIFY_STAGING_HTTP_AUTH_PASSWORD }} + NOTIFY_STAGING_HTTP_AUTH_USER: ${{ secrets.NOTIFY_STAGING_HTTP_AUTH_USER }} + NOTIFY_STAGING_URI: ${{ secrets.NOTIFY_STAGING_URI }} - name: Check coverage threshold run: pipenv run coverage report --fail-under=90 diff --git a/Makefile b/Makefile index 709e8fe02..2d0a1ee67 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e .PHONY: bootstrap bootstrap: generate-version-file ## Set up everything to run the app pipenv install --dev + pipenv run playwright install --with-deps source $(NVMSH) --no-use && nvm install && npm ci --no-audit source $(NVMSH) && npm run build @@ -54,10 +55,15 @@ py-lint: ## Run python linting scanners .PHONY: py-test py-test: export NEW_RELIC_ENVIRONMENT=test py-test: ## Run python unit tests - pipenv run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 tests/ + pipenv run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 --ignore=tests/end_to_end tests/ pipenv run coverage report --fail-under=90 pipenv run coverage html -d .coverage_cache +.PHONY: e2e-test +e2e-test: export NEW_RELIC_ENVIRONMENT=test +e2e-test: ## Run end-to-end integration tests + pipenv run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end + .PHONY: js-lint js-lint: ## Run javascript linting scanners source $(NVMSH) && npm run lint diff --git a/Pipfile b/Pipfile index 93f9739cb..325e2470d 100644 --- a/Pipfile +++ b/Pipfile @@ -37,6 +37,7 @@ newrelic = "*" flask-talisman = "*" notifications-utils = {editable = true, ref = "main", git = "https://github.com/GSA/notifications-utils.git"} coverage = "*" +pytest-playwright = "*" [dev-packages] isort = "==5.12.0" diff --git a/Pipfile.lock b/Pipfile.lock index d6501d00b..520f7b91c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7d90c44fa5dd863a7ebab1ec4fa02b8d5dc11050235d7bad87ee6cfde791720e" + "sha256": "e6e49305bdee7cdd4605b8f6f7d0135cfe91e0c038abe6efdfa1784194ce3cdd" }, "pipfile-spec": 6, "requires": { @@ -50,19 +50,19 @@ }, "boto3": { "hashes": [ - "sha256:cfcb20d5784428f31d89889e68b26efeda90f231c3119eef4af8b25ad405c55f", - "sha256:d5ac6599951fdd519ed26c6fe15c41a7aa4021cb9adce33167344f8ce5cdb07b" + "sha256:b2d178c8a56fe3e4c9b123dccdff20e9555d12a597b72627fa659aa6295e238a", + "sha256:db6443fd2c65d9f35f671b03bacb0592b62d06884395ed65d75922ccddc34c2e" ], "markers": "python_version >= '3.7'", - "version": "==1.28.12" + "version": "==1.28.13" }, "botocore": { "hashes": [ - "sha256:7e5db466c762a071bb58c9a39d070f1333ce4f4ba6fdf9820ba21e87bd4c7e29", - "sha256:86380672151866b5e425636e3ebad74f2b83e7163e36ef5d38d11a04b9cba33b" + "sha256:78b96afbd88b8bd4c0967611a4cedddd9ea33d8601309dc351f81cbb5479d976", + "sha256:9a5080ea2a444f0447a7a1a79f64252ae2a1417b6c13a54656ee991cb610dd4e" ], "markers": "python_version >= '3.7'", - "version": "==1.31.12" + "version": "==1.31.13" }, "cachetools": { "hashes": [ @@ -235,7 +235,7 @@ "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==3.2.0" }, "click": { @@ -556,6 +556,14 @@ "markers": "python_version < '3.10'", "version": "==6.8.0" }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, "itsdangerous": { "hashes": [ "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", @@ -826,6 +834,14 @@ ], "version": "==2.0.3" }, + "packaging": { + "hashes": [ + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1" + }, "phonenumbers": { "hashes": [ "sha256:89671217c706cbaa3ced101deefafa779836feac3e059434d886ac31f09f32c0", @@ -833,6 +849,27 @@ ], "version": "==8.13.17" }, + "playwright": { + "hashes": [ + "sha256:428a719a6c7e40781c19860ed813840ac2d63678f7587abe12e800ea030d4b7e", + "sha256:4e396853034742b76654cdab27422155d238f46e4dc6369ea75854fafb935586", + "sha256:72e80076e595f5fcd8ebd89bf6635ad78e4bafa633119faed8b2568d17dbd398", + "sha256:84213339f179fd2a70f77ea7faea0616d74871349d556c53a1ecb7dd5097973c", + "sha256:89ca2261bb00b67d3dff97691cf18f4347ee0529a11e431e47df67b703d4d8fa", + "sha256:b7c6ddfca2b141b0385387cc56c125b14ea867902c39e3fc650ddd6c429b17da", + "sha256:ffbb927679b62fad5071439d5fe0840af46ad1844bc44bf80e1a0ad706140c98" + ], + "markers": "python_version >= '3.8'", + "version": "==1.36.0" + }, + "pluggy": { + "hashes": [ + "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", + "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.0" + }, "prometheus-client": { "hashes": [ "sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091", @@ -848,6 +885,13 @@ ], "version": "==2.21" }, + "pyee": { + "hashes": [ + "sha256:2770c4928abc721f46b705e6a72b0c59480c4a69c9a83ca0b00bb994f1ea4b32", + "sha256:9f066570130c554e9cc12de5a9d86f57c7ee47fece163bbdaa3e9c933cfbdfa5" + ], + "version": "==9.0.4" + }, "pyexcel": { "hashes": [ "sha256:ddc6904512bfa2ecda509fb3b58229bb30db14498632fd9e7a5ba7bbfb02ed1b", @@ -942,6 +986,30 @@ "index": "pypi", "version": "==3.6.0" }, + "pytest": { + "hashes": [ + "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", + "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a" + ], + "markers": "python_version >= '3.7'", + "version": "==7.4.0" + }, + "pytest-base-url": { + "hashes": [ + "sha256:e1e88a4fd221941572ccdcf3bf6c051392d2f8b6cef3e0bc7da95abec4b5346e", + "sha256:ed36fd632c32af9f1c08f2c2835dcf42ca8fcd097d6ed44a09f253d365ad8297" + ], + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==2.0.0" + }, + "pytest-playwright": { + "hashes": [ + "sha256:83a896b1b28bfaa081ca9ea27229a06a114e106e2e62fb3d5f06544748fbc1fe", + "sha256:9bf79c633c97dd1405308b8d3600e6c8c2a200a733e2f36c5a150ba4701936f8" + ], + "index": "pypi", + "version": "==0.3.3" + }, "python-dateutil": { "hashes": [ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", @@ -966,6 +1034,14 @@ "markers": "python_version >= '3.6'", "version": "==2.0.7" }, + "python-slugify": { + "hashes": [ + "sha256:70ca6ea68fe63ecc8fa4fcf00ae651fc8a5d02d93dcd12ae6d4fc7ca46c4d395", + "sha256:ce0d46ddb668b3be82f4ed5e503dbc33dd815d83e2eb6824211310d3fb172a27" + ], + "markers": "python_version >= '3.7'", + "version": "==8.0.1" + }, "pytz": { "hashes": [ "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588", @@ -1118,6 +1194,13 @@ ], "version": "==2.0.1" }, + "text-unidecode": { + "hashes": [ + "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", + "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" + ], + "version": "==1.3" + }, "texttable": { "hashes": [ "sha256:290348fb67f7746931bcdfd55ac7584ecd4e5b0846ab164333f0794b121760f2", @@ -1125,6 +1208,14 @@ ], "version": "==1.6.7" }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, "typing-extensions": { "hashes": [ "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", @@ -1215,19 +1306,19 @@ }, "boto3": { "hashes": [ - "sha256:cfcb20d5784428f31d89889e68b26efeda90f231c3119eef4af8b25ad405c55f", - "sha256:d5ac6599951fdd519ed26c6fe15c41a7aa4021cb9adce33167344f8ce5cdb07b" + "sha256:b2d178c8a56fe3e4c9b123dccdff20e9555d12a597b72627fa659aa6295e238a", + "sha256:db6443fd2c65d9f35f671b03bacb0592b62d06884395ed65d75922ccddc34c2e" ], "markers": "python_version >= '3.7'", - "version": "==1.28.12" + "version": "==1.28.13" }, "botocore": { "hashes": [ - "sha256:7e5db466c762a071bb58c9a39d070f1333ce4f4ba6fdf9820ba21e87bd4c7e29", - "sha256:86380672151866b5e425636e3ebad74f2b83e7163e36ef5d38d11a04b9cba33b" + "sha256:78b96afbd88b8bd4c0967611a4cedddd9ea33d8601309dc351f81cbb5479d976", + "sha256:9a5080ea2a444f0447a7a1a79f64252ae2a1417b6c13a54656ee991cb610dd4e" ], "markers": "python_version >= '3.7'", - "version": "==1.31.12" + "version": "==1.31.13" }, "cachecontrol": { "extras": [ @@ -1395,7 +1486,7 @@ "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==3.2.0" }, "cryptography": { @@ -1782,7 +1873,7 @@ "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==32.0.1" }, "pluggy": { @@ -1845,7 +1936,7 @@ "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a" ], - "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==7.4.0" }, "pytest-env": { @@ -1955,7 +2046,7 @@ "sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec", "sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==13.4.2" }, "s3transfer": { diff --git a/README.md b/README.md index bbb298684..202b69e17 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ The [Notify API](https://github.com/GSA/notifications-api) provides the UI's bac ### Common steps -1. Install pre-requisites for setup: +1. Install pre-requisites for setup (on a Mac): + - Install XCode or at least the XCode Command Line Tools + - [Homebrew](https://brew.sh/) (follow instructions on page) - [jq](https://stedolan.github.io/jq/): `brew install jq` - [terraform](https://www.terraform.io/): `brew install terraform` or `brew install tfenv` and use `tfenv` to install `terraform ~> 1.4.0` - [cf-cli@8](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html): `brew install cloudfoundry/tap/cf-cli@8` @@ -44,6 +46,23 @@ The [Notify API](https://github.com/GSA/notifications-api) provides the UI's bac `make bootstrap` + If you run into certificate errors at the `playwright install` step, try doing this: + + 1. Run `brew --prefix` to see Homebrew's root directory + + 1. Create or modify the local `.env` file in the project and add this line: + + `NODE_EXTRA_CA_CERTS=/CHANGE-TO-HOMEBREW-INSTALL-PATH/etc/ca-certificates/cert.pem` + + Make sure to change `CHANGE-TO-HOMEBREW-INSTALL-PATH` to the path given by `brew --prefix` in the step above. + For example, if `brew --prefix` gave `/opt/homebrew` as output, then the line would look like this: + + `NODE_EXTRA_CA_CERTS=/opt/homebrew/etc/ca-certificates/cert.pem` + + 1. Save the changes to the `.env` file + + 1. Run `make bootstrap` again + 1. Run the Flask server `make run-flask` diff --git a/docs/end_to_end_tests.md b/docs/end_to_end_tests.md new file mode 100644 index 000000000..906200a1c --- /dev/null +++ b/docs/end_to_end_tests.md @@ -0,0 +1,120 @@ +# Working with End-to-End Tests + +End-to-End (E2E) tests are an important part of a assessing the overall +integrity and stability of a system. They are a part of the overall +test suite and serve the function of simulating a user working through +the application. By having comprehensive E2E tests in place, we can +instill higher confidence that future changes and refactorings won't +negatively impact any user experience or break existing functionality. + +The US Notify project leverages [`pytest`](https://pytest.org/) for its +existing test suite (at least on the Python side of things) and is now +leveraging [Playwright for Python](https://playwright.dev/python/) +along with its `pytest` plugin for the E2E tests. + + +## Getting Started + +To work with the E2E tests in US Notify, you need to make sure you have +all of the necessary components installed. The quick and easy way to do +this is to use the Makefile as you did for the initial project setup. In +fact, if you've already done this, you are already set to go! If not, +then run the bootstrap command in your shell: + +```sh +make bootstrap +``` + +This takes care of installing all of your dependencies, including those +now needed for Playwright. + +If you run into certificate errors at the `playwright install` step, try +doing this: + +1. Run `brew --prefix` to see Homebrew's root directory + +1. Create or modify the local `.env` file in the project and add this line: + + `NODE_EXTRA_CA_CERTS=/CHANGE-TO-HOMEBREW-INSTALL-PATH/etc/ca-certificates/cert.pem` + + Make sure to change `CHANGE-TO-HOMEBREW-INSTALL-PATH` to the path + given by `brew --prefix` in the step above. For example, if `brew --prefix` + gave `/opt/homebrew` as output, then the line would look like this: + + `NODE_EXTRA_CA_CERTS=/opt/homebrew/etc/ca-certificates/cert.pem` + +1. Save the changes to the `.env` file + +1. Run `make bootstrap` again + + +### Manual Installation + +If you need to install things separately, you'll still need to make sure +your environment is set up and configured as outlined in the README. + +At your shell in the project root folder, run the following commands: + +```sh +pipenv install pytest-playwright +pipenv run playwright install --with-deps +``` + +This will install Playwright and its `pytest` plugin, then the +additional dependencies that Playwright requires. + +See more details on the [Playwright for Python Installation page](https://playwright.dev/python/docs/intro). + + +## Local Configuration + +In order to run the E2E tests successfully on your local machine, you'll also +need to make sure you have a `.env` file in the root project folder, and that it +has at least these environment variables set in it: + +``` +NOTIFY_STAGING_URI +NOTIFY_STAGING_HTTP_AUTH_USER +NOTIFY_STAGING_HTTP_AUTH_PASSWORD +``` + +This file is **not** checked into source control and is configured to be +ignored in the project's `.gitignore` file; please be careful that it is +not committed to the repo and pushed! + + +## Running E2E Tests Locally + +To run the E2E tests on your local machine, type this command in your +shell at the project root directory: + +```sh +make e2e-test +``` + +You should see `pytest` start producing output and the existing E2E +tests run in multiple headless browsers. + + +## How to Create and Maintain E2E Tests + +All of the E2E tests are found in the `tests/end_to_end` folder and are +written as `pytest` scripts using [Playwright's Python Framework](https://playwright.dev/python/docs/writing-tests). + + +## Maintaining E2E Tests with GitHub + +The E2E tests are configured to run as a separate GitHub action as a +part of our other checks found in `.github/workflows/checks.yml`. + +The E2E tests are not run as a part of the regular unit test suite; if +you look at the `Makefile` you'll see that the tests are two separate +commands, with the E2E tests configured separately. + +This is done for a couple of reasons: + +- Keeps unit tests isolated from the E2E tests +- Allows us to configure E2E tests separately + +The environment variables are managed as a part of the GitHub +repository settings. diff --git a/tests/conftest.py b/tests/conftest.py index 79aa06b8c..69ab8864e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3384,3 +3384,15 @@ def webauthn_credential_2(): 'registration_response': 'stuff', 'created_at': '2021-05-14T16:57:14.154185Z', } + + +@pytest.fixture(scope='session') +def end_to_end_auth_context(browser): + # Create a context with HTTP Authentication credentials for Playwright E2E + # tests. + context = browser.new_context(http_credentials={ + 'username': os.environ.get('NOTIFY_STAGING_HTTP_AUTH_USER'), + 'password': os.environ.get('NOTIFY_STAGING_HTTP_AUTH_PASSWORD'), + }) + + yield context diff --git a/tests/end_to_end/test_landing_and_sign_in_pages.py b/tests/end_to_end/test_landing_and_sign_in_pages.py new file mode 100644 index 000000000..82e7d02eb --- /dev/null +++ b/tests/end_to_end/test_landing_and_sign_in_pages.py @@ -0,0 +1,109 @@ +import os +import re + +from playwright.sync_api import expect + + +def test_landing_page(end_to_end_auth_context): + # Open a new page and go to the staging site. + page = end_to_end_auth_context.new_page() + page.goto(os.environ.get('NOTIFY_STAGING_URI')) + + # Check the page title exists and matches what we expect. + expect(page).to_have_title(re.compile('U.S. Notify')) + + # Retrieve some prominent elements on the page for testing. + main_header = page.get_by_role( + 'heading', + name='Send text messages to your participants' + ) + sign_in_button = page.get_by_role('link', name='Sign in') + benefits_studio_email = page.get_by_role( + 'link', + name='tts-benefits-studio@gsa.gov' + ) + + # Check to make sure the elements are visible. + expect(main_header).to_be_visible() + expect(sign_in_button).to_be_visible() + expect(benefits_studio_email).to_be_visible() + + # Check to make sure the sign-in button and email links are correct. + expect(sign_in_button).to_have_attribute('href', '/sign-in') + expect(benefits_studio_email).to_have_attribute( + 'href', + 'mailto:tts-benefits-studio@gsa.gov' + ) + + # Retrieve all other main content headers and check that they're + # visible. + content_headers = [ + 'Control your content', + 'See how your messages perform', + 'No technical integration needed', + 'About the product', + ] + + for content_header in content_headers: + expect( + page.get_by_role('heading', name=re.compile(content_header)) + ).to_be_visible() + + +def test_sign_in_page(end_to_end_auth_context): + # Open a new page and go to the staging site. + page = end_to_end_auth_context.new_page() + page.goto(os.environ.get('NOTIFY_STAGING_URI')) + + sign_in_button = page.get_by_role('link', name='Sign in') + + # Test trying to sign in. + sign_in_button.click() + + # Check the page title exists and matches what we expect. + # NOTE: The dash is a special character! It had to be copied from + # the template itself. + # TODO: Improve this check, or change it so no special character is + # needed. Better yet, fix the template(s) character too. + expect(page).to_have_title(re.compile('Sign in – U.S. Notify')) + + # Check for the sign in heading. + sign_in_heading = page.get_by_role('heading', name='Sign in') + expect(sign_in_heading).to_be_visible() + + # Check for the sign in form elements. + # NOTE: Playwright cannot find input elements by role and recommends using + # get_by_label() instead; however, hidden form elements do not have + # labels associated with them, hence the XPath! + # See https://playwright.dev/python/docs/api/class-page#page-get-by-label + # and https://playwright.dev/python/docs/locators#locate-by-css-or-xpath + # for more information. + email_address_input = page.get_by_label('Email address') + password_input = page.get_by_label('Password') + csrf_token = page.locator('xpath=//input[@name="csrf_token"]') + continue_button = page.get_by_role('button', name=re.compile('Continue')) + forgot_password_link = page.get_by_role( + 'link', + name='Forgot your password?' + ) + + # Make sure form elements are visible and not visible as expected. + expect(email_address_input).to_be_visible() + expect(password_input).to_be_visible() + expect(continue_button).to_be_visible() + expect(forgot_password_link).to_be_visible() + + expect(csrf_token).to_be_hidden() + + # Make sure form elements are configured correctly with the right + # attributes. + expect(email_address_input).to_have_attribute('type', 'email') + expect(password_input).to_have_attribute('type', 'password') + expect(csrf_token).to_have_attribute('type', 'hidden') + expect(continue_button).to_have_attribute('type', 'submit') + expect(forgot_password_link).to_have_attribute( + 'href', + '/forgot-password' + ) + + # TODO: Figure out how to actually sign in...