Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: smoke tests #2623

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5acf79a
feat: add setup for a separate container to run playwright
angela-tran Dec 2, 2024
6bb718f
feat(playwright): add healthcheck test and helper script for running
angela-tran Dec 2, 2024
620c0fe
chore: add pytest-playwright to devcontainer
angela-tran Dec 2, 2024
893afe8
refactor: extract some helper script args out into pytest.ini file
angela-tran Dec 2, 2024
ec0ed68
fix(ci): specify directory to use for running unit tests
angela-tran Dec 2, 2024
45de268
refactor: make playwright service's default command be to run the tests
angela-tran Dec 2, 2024
0dd0385
chore: pin version of Playwright, set up dependabot
angela-tran Jan 3, 2025
98b4162
feat: configure Playwright service to allow running in headed mode
angela-tran Dec 3, 2024
dc64d55
refactor: move healthcheck test into a healthcheck folder
angela-tran Jan 9, 2025
ccea107
refactor: specify base URL as CLI argument
angela-tran Jan 9, 2025
36bfac6
feat: add agency flow smoke test
angela-tran Jan 9, 2025
234966b
feat: add working older adult flow test with one-time password as MFA
angela-tran Jan 10, 2025
9098c4b
feat: add veteran and calfresh Login.gov flows
angela-tran Jan 10, 2025
320e18c
refactor(agency_card): use Page object models for readability and reuse
angela-tran Jan 10, 2025
e979f72
refactor(login_gov): use Page object model pattern for older adult flow
angela-tran Jan 11, 2025
fc158f8
refactor(login_gov): use same pattern for veteran and calfresh flow
angela-tran Jan 11, 2025
228b856
feat: add working Medicare.gov flow test
angela-tran Jan 11, 2025
9500dea
refactor(medicare_gov): use Page object model pattern
angela-tran Jan 11, 2025
7a7e220
refactor: use special User-Agent for smoke testing
angela-tran Jan 11, 2025
26388e7
chore: add comment linking to Playwright docs on page object models
angela-tran Jan 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,21 @@ updates:
include: "scope"
labels:
- "dependencies"
- package-ecosystem: "docker"
directory: "/tests/playwright" # Playwright Docker image
schedule:
interval: "daily"
commit-message:
prefix: "chore"
include: "scope"
labels:
- "dependencies"
- package-ecosystem: "pip"
directory: "/tests/playwright" # Playwright pytest plugin
schedule:
interval: "daily"
commit-message:
prefix: "chore"
include: "scope"
labels:
- "dependencies"
11 changes: 11 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,14 @@ services:
- "8000"
volumes:
- ./.devcontainer:/.devcontainer

playwright:
build:
context: .
dockerfile: tests/playwright/Dockerfile
env_file: .env
image: benefits_client:playwright
volumes:
- ./tests/playwright:/playwright
- /tmp/.X11-unix:/tmp/.X11-unix
command: ./run.sh
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ test = [
"pytest",
"pytest-django",
"pytest-mock",
"pytest-playwright", # only for writing tests. run tests using playwright Docker Compose service
"pyotp",
"pytest-socket",
]

Expand Down
3 changes: 3 additions & 0 deletions tests/playwright/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__
.pytest_cache
test-results
11 changes: 11 additions & 0 deletions tests/playwright/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# https://playwright.dev/docs/docker
FROM mcr.microsoft.com/playwright/python:v1.48.0-jammy

WORKDIR /playwright

COPY tests/playwright/requirements.txt requirements.txt

RUN python -m pip install --upgrade pip && \
pip install -r requirements.txt

USER pwuser
7 changes: 7 additions & 0 deletions tests/playwright/healthcheck_tests/test_healthcheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from playwright.sync_api import Page, expect


def test_dev_healthcheck(page: Page):
page.goto("/healthcheck")

expect(page.get_by_text("Healthy")).to_be_visible()
2 changes: 2 additions & 0 deletions tests/playwright/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
addopts = --tracing on -v --template=html1/index.html --report=test-results/report.html --video on
5 changes: 5 additions & 0 deletions tests/playwright/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
playwright==1.48.0 # needs to match version on Docker image
pyotp
pytest
pytest-playwright
pytest-reporter-html1
7 changes: 7 additions & 0 deletions tests/playwright/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

set -e

BASE_URL="https://dev-benefits.calitp.org"

pytest --base-url $BASE_URL
154 changes: 154 additions & 0 deletions tests/playwright/smoke_tests/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""
Page object models for Playwright tests.

https://playwright.dev/python/docs/pom
"""

from playwright.sync_api import Page


class Index:
def __init__(self, page: Page):
self.page = page

def select_agency(self, agency_name):
page = self.page
page.get_by_role("link", name="Choose your Provider").click()
page.get_by_role("link", name=agency_name).click()

return EligibilityIndex(page)


class EligibilityIndex:
def __init__(self, page: Page):
self.page = page

def select_flow(self, flow_name):
page = self.page
page.get_by_label(flow_name).check()
page.wait_for_load_state("networkidle") # wait for reCAPTCHA to finish loading
page.get_by_role("button", name="Choose this benefit").click()

return EligibilityStart(page)


class EligibilityStart:
def __init__(self, page: Page):
self.page = page

def click_continue(self):
page = self.page
page.get_by_role("button", name="Continue").click()

return EligibilityConfirm(page)

def get_started_with_login_gov(self):
page = self.page
page.get_by_role("link", name="Get started with Login.gov").click()

return LoginGov(page)

def continue_to_medicare_gov(self):
page = self.page
page.get_by_role("button", name="Continue to Medicare.gov").click()

return MedicareGov(page)


class EligibilityConfirm:
def __init__(self, page: Page):
self.page = page

def submit_form(self, sub, name):
page = self.page
page.get_by_placeholder("12345").click()

page.get_by_placeholder("12345").fill(sub)
page.keyboard.press("Tab")

page.get_by_placeholder("Hernandez-Demarcos").fill(name)

page.get_by_role("button", name="Find my record").click()

return EnrollmentIndex(page)


class LoginGov:
def __init__(self, page: Page):
self.page = page

def sign_in(self, username, password):
page = self.page
page.get_by_label("Email address").click()
page.get_by_label("Email address").fill(username)
page.get_by_label("Email address").press("Tab")

page.get_by_label("Password", exact=True).fill(password)

page.get_by_role("button", name="Sign in").click()

def enter_otp(self, one_time_password):
page = self.page
page.get_by_label("One-time code").click()
page.get_by_label("One-time code").fill(one_time_password)

page.get_by_role("button", name="Submit").click()

return EnrollmentIndex(page)


class MedicareGov:
def __init__(self, page: Page):
self.page = page

def log_in(self, username, password):
page = self.page

page.get_by_label("Username", exact=True).click()
page.get_by_label("Username", exact=True).fill(username)

page.get_by_label("Password").fill(password)

page.get_by_role("button", name="Log in").click()

def accept_consent_screen(self):
page = self.page
page.get_by_role("button", name="Connect").click()

return EnrollmentIndex(page)


class EnrollmentIndex:
def __init__(self, page: Page):
self.page = page

def enroll(self, cardholder_name, card_number, expiration, security_code):
page = self.page

with page.expect_popup() as popup_info:
page.get_by_role("button", name="Enroll").click()

popup = popup_info.value
popup.wait_for_timeout(3000)

popup.get_by_text("Cardholder name").click()

popup.get_by_label("Cardholder name").fill(cardholder_name)
popup.keyboard.press("Tab")

popup.get_by_label("Card number").fill(card_number)
popup.keyboard.press("Tab")

popup.get_by_label("mm/yy").fill(expiration)
popup.keyboard.press("Tab")

popup.get_by_text("Security code", exact=True).click()
popup.get_by_label("Security code").fill(security_code)

# trigger form validation - not sure why their form behaves this way
popup.keyboard.press("Shift+Tab")
popup.keyboard.press("Shift+Tab")
popup.keyboard.press("Shift+Tab")
popup.keyboard.press("Tab")

popup.get_by_role("group", name="Enter your card details").get_by_role("button").click()
23 changes: 23 additions & 0 deletions tests/playwright/smoke_tests/test_agency_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from playwright.sync_api import Browser, expect

from models import Index


def test_agency_card_flow(browser: Browser, base_url):
context = browser.new_context(user_agent="cal-itp/benefits-smoke-test")
page = context.new_page()

page.goto(base_url)

index = Index(page)
eligibility_index = index.select_agency("California State Transit")
eligibility_start = eligibility_index.select_flow("Agency Cardholder")
eligibility_confirm = eligibility_start.click_continue()
enrollment_index = eligibility_confirm.submit_form("71162", "Box")
enrollment_index.enroll("Test User", "4111 1111 1111 1111", "12/34", "123")

# enrollment can take a bit
page.wait_for_timeout(10000)

success_message = page.get_by_text("You can now use your contactless card to tap to ride with a reduced fare!")
expect(success_message).to_be_visible()
96 changes: 96 additions & 0 deletions tests/playwright/smoke_tests/test_login_gov_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import os

from playwright.sync_api import Browser, expect
import pyotp

from models import Index


def test_older_adult_flow(browser: Browser, base_url):
context = browser.new_context(user_agent="cal-itp/benefits-smoke-test")
page = context.new_page()

page.goto(base_url)

index = Index(page)
eligibility_index = index.select_agency("California State Transit")
eligibility_start = eligibility_index.select_flow("Older Adult")

login_gov = eligibility_start.get_started_with_login_gov()

username = os.environ.get("PLAYWRIGHT_LOGIN_GOV_OLDER_ADULT_USERNAME")
password = os.environ.get("PLAYWRIGHT_LOGIN_GOV_OLDER_ADULT_PASSWORD")
login_gov.sign_in(username, password)

authenticator_secret = os.environ.get("PLAYWRIGHT_LOGIN_GOV_OLDER_ADULT_AUTHENTICATOR_SECRET")
# create instance of "authenticator app"
totp = pyotp.TOTP(authenticator_secret)
enrollment_index = login_gov.enter_otp(totp.now())

enrollment_index.enroll("Test User", "4111 1111 1111 1111", "12/34", "123")

# enrollment can take a bit
page.wait_for_timeout(10000)

success_message = page.get_by_text("You can now use your contactless card to tap to ride with a reduced fare!")
expect(success_message).to_be_visible()


def test_us_veteran_flow(browser: Browser, base_url):
context = browser.new_context(user_agent="cal-itp/benefits-smoke-test")
page = context.new_page()

page.goto(base_url)

index = Index(page)
eligibility_index = index.select_agency("California State Transit")
eligibility_start = eligibility_index.select_flow("U.S. Veteran")

login_gov = eligibility_start.get_started_with_login_gov()

username = os.environ.get("PLAYWRIGHT_LOGIN_GOV_VETERAN_USERNAME")
password = os.environ.get("PLAYWRIGHT_LOGIN_GOV_VETERAN_PASSWORD")
login_gov.sign_in(username, password)

authenticator_secret = os.environ.get("PLAYWRIGHT_LOGIN_GOV_VETERAN_AUTHENTICATOR_SECRET")
# create instance of "authenticator app"
totp = pyotp.TOTP(authenticator_secret)
enrollment_index = login_gov.enter_otp(totp.now())

enrollment_index.enroll("Test User", "4111 1111 1111 1111", "12/34", "123")

# enrollment can take a bit
page.wait_for_timeout(10000)

success_message = page.get_by_text("You can now use your contactless card to tap to ride with a reduced fare!")
expect(success_message).to_be_visible()


def test_calfresh_cardholder_flow(browser: Browser, base_url):
context = browser.new_context(user_agent="cal-itp/benefits-smoke-test")
page = context.new_page()

page.goto(base_url)

index = Index(page)
eligibility_index = index.select_agency("California State Transit")
eligibility_start = eligibility_index.select_flow("CalFresh Cardholder")

login_gov = eligibility_start.get_started_with_login_gov()

username = os.environ.get("PLAYWRIGHT_LOGIN_GOV_CALFRESH_USERNAME")
password = os.environ.get("PLAYWRIGHT_LOGIN_GOV_CALFRESH_PASSWORD")
login_gov.sign_in(username, password)

authenticator_secret = os.environ.get("PLAYWRIGHT_LOGIN_GOV_CALFRESH_AUTHENTICATOR_SECRET")
# create instance of "authenticator app"
totp = pyotp.TOTP(authenticator_secret)
enrollment_index = login_gov.enter_otp(totp.now())

enrollment_index.enroll("Test User", "4111 1111 1111 1111", "12/34", "123")

# enrollment can take a bit
page.wait_for_timeout(10000)

reenrollment_error_message = page.get_by_text("You are still enrolled in this benefit")
expect(reenrollment_error_message).to_be_visible()
33 changes: 33 additions & 0 deletions tests/playwright/smoke_tests/test_medicare_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
from playwright.sync_api import Browser, expect

from models import Index


def test_medicare_cardholder_flow(browser: Browser, base_url):
context = browser.new_context(user_agent="cal-itp/benefits-smoke-test")
page = context.new_page()

page.goto(base_url)

index = Index(page)
eligibility_index = index.select_agency("California State Transit")
eligibility_start = eligibility_index.select_flow("Medicare Cardholder")

# avoid looking like a bot
page.add_init_script("delete Object.getPrototypeOf(navigator).webdriver")

medicare_gov = eligibility_start.continue_to_medicare_gov()

username = os.environ.get("PLAYWRIGHT_MEDICARE_GOV_USERNAME")
password = os.environ.get("PLAYWRIGHT_MEDICARE_GOV_PASSWORD")
medicare_gov.log_in(username, password)
enrollment_index = medicare_gov.accept_consent_screen()

enrollment_index.enroll("Test User", "4111 1111 1111 1111", "12/34", "123")

# enrollment can take a bit
page.wait_for_timeout(10000)

success_message = page.get_by_text("You can now use your contactless card to tap to ride with a reduced fare!")
expect(success_message).to_be_visible()
Loading
Loading