diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..86228c8 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +TEST_EMAIL= +TEST_PASSWORD= diff --git a/.github/workflows/demo-tests.yml b/.github/workflows/demo-tests.yml new file mode 100644 index 0000000..85912a2 --- /dev/null +++ b/.github/workflows/demo-tests.yml @@ -0,0 +1,34 @@ +name: Demo Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + demo: + timeout-minutes: 10 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + + - name: Install Playwright browsers + run: python -m playwright install --with-deps chromium + + - name: Run demo tests + run: python -m pytest -m demo -v diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c1a49c5..f414298 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,10 +1,7 @@ -name: Playwright E2E Tests +name: Live Seznam E2E (manual) on: - push: - branches: [main, master] - pull_request: - branches: [main, master] + workflow_dispatch: jobs: test: @@ -27,10 +24,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install playwright pytest pytest-bdd python-dotenv + python -m pip install -r requirements.txt - name: Install Playwright browsers run: python -m playwright install --with-deps chromium - - name: Run Pytest BDD tests - run: python -m pytest + - name: Run live Seznam tests + run: python -m pytest -m live diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a90b00f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 MilJav11 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 3804f9b..0887b10 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Playwright E2E Automation Portfolio Project -[![Playwright E2E Tests](https://github.com/MilJav11/qa-automation-annea/actions/workflows/tests.yml/badge.svg)](https://github.com/MilJav11/qa-automation-annea/actions/workflows/tests.yml) +[![Demo Tests](https://github.com/MilJav11/playwright-python-e2e-framework/actions/workflows/demo-tests.yml/badge.svg)](https://github.com/MilJav11/playwright-python-e2e-framework/actions/workflows/demo-tests.yml) Professional E2E test suite automating the Seznam.cz email workflow using Playwright and Python. @@ -9,7 +9,7 @@ Professional E2E test suite automating the Seznam.cz email workflow using Playwr This test suite covers the critical business path (Happy Path) of sending an email via the Seznam.cz web client: 1. Successfully log into a Seznam.cz email account (handling dynamic cookie/ad banners). -2. Navigate directly to the mailbox and initialize a new email to a predefined contact. +2. Navigate directly to the mailbox and initialize a new email to a contact configured via `TEST_EMAIL`. 3. Dynamically generate and attach a local text document (`test_attachment.txt`). 4. Send the email and explicitly wait for the UI notification verifying successful delivery. 5. Safely log out of the account via the profile menu. @@ -32,13 +32,18 @@ As an E2E UI automation suite, coverage is focused on **Functional Business Flow ## 📁 Project Structure ```text -├── .github/workflows/ # CI/CD pipeline definition -├── pages/ # Page Object Model classes +├── .github/workflows/ +│ ├── demo-tests.yml # Gating CI: credential-free demo suite +│ └── tests.yml # Manual live Seznam suite (workflow_dispatch) +├── pages/ # Page Object Model classes (Seznam + the-internet demo) ├── tests/ -│ ├── step_definitions/ # Python code executing the BDD steps -│ ├── email_workflow.feature # Gherkin scenario +│ ├── features/ # Gherkin: email_workflow.feature (@live), demo_the_internet.feature (@demo) +│ ├── step_definitions/ # BDD steps: test_email_steps.py, test_demo_steps.py │ └── conftest.py # Playwright fixtures and setup +├── .env.example # Template for environment variables +├── pytest.ini # Pytest configuration ├── requirements.txt # Python dependencies +├── test_attachment.txt # Sample attachment used by the tests └── README.md ``` @@ -47,18 +52,17 @@ As an E2E UI automation suite, coverage is focused on **Functional Business Flow ### Prerequisites - Python 3.10+ -- Node.js (for Playwright binaries) ### Installation 1. Clone the repository: ```bash -git clone https://github.com/MilJav11/qa-automation-annea.git -cd qa-automation-annea +git clone https://github.com/MilJav11/playwright-python-e2e-framework.git +cd playwright-python-e2e-framework ``` -2. Create a `.env` file in the root directory and add your credentials: +2. Configuration: copy `.env.example` to `.env` in the root directory and fill in your credentials. Use a dedicated Seznam TEST account for this purpose. Never commit your `.env` file — it is git-ignored. In CI/CD, `TEST_EMAIL` and `TEST_PASSWORD` are read from repository secrets. ``` TEST_EMAIL=your_test_email@seznam.cz @@ -77,10 +81,18 @@ pip install -r requirements.txt playwright install chromium ``` -### Running Tests +### Demo suite (no account needed) -Execute the test suite using pytest: +Runs against the public automation site https://the-internet.herokuapp.com (login, invalid password, file upload, logout). No credentials or `.env` required. This is the suite that gates CI. ```bash -pytest +pytest -m demo -v +``` + +### Live Seznam suite + +The Seznam email scenario (`@live`) needs a dedicated test account (`TEST_EMAIL` / `TEST_PASSWORD`). It is run manually (GitHub Actions `workflow_dispatch`, or locally) because the third-party Seznam UI changes and can break the test independently of this code. + +```bash +pytest -m live -v ``` diff --git a/pages/the_internet_login_page.py b/pages/the_internet_login_page.py new file mode 100644 index 0000000..eee4c7f --- /dev/null +++ b/pages/the_internet_login_page.py @@ -0,0 +1,16 @@ +from .base_page import BasePage + + +class TheInternetLoginPage(BasePage): + URL = "https://the-internet.herokuapp.com/login" + + def open(self): + self.navigate(self.URL) + + def login(self, username, password): + self.page.locator("#username").fill(username) + self.page.locator("#password").fill(password) + self.page.get_by_role("button", name="Login").click() + + def flash_message(self): + return self.page.locator("#flash") diff --git a/pages/the_internet_secure_page.py b/pages/the_internet_secure_page.py new file mode 100644 index 0000000..504e3d5 --- /dev/null +++ b/pages/the_internet_secure_page.py @@ -0,0 +1,9 @@ +from .base_page import BasePage + + +class TheInternetSecurePage(BasePage): + def heading(self): + return self.page.get_by_role("heading", level=2) + + def logout(self): + self.page.get_by_role("link", name="Logout").click() diff --git a/pages/the_internet_upload_page.py b/pages/the_internet_upload_page.py new file mode 100644 index 0000000..2856c12 --- /dev/null +++ b/pages/the_internet_upload_page.py @@ -0,0 +1,15 @@ +from .base_page import BasePage + + +class TheInternetUploadPage(BasePage): + URL = "https://the-internet.herokuapp.com/upload" + + def open(self): + self.navigate(self.URL) + + def upload(self, file_path): + self.page.locator("#file-upload").set_input_files(str(file_path)) + self.page.locator("#file-submit").click() + + def uploaded_files(self): + return self.page.locator("#uploaded-files") diff --git a/pytest.ini b/pytest.ini index c9f3bfa..7b1d952 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,4 +3,6 @@ pythonpath = . bdd_features_base_dir = tests/features markers = level3: Level 3 test - smoke: Smoke test \ No newline at end of file + smoke: Smoke test + demo: Credential-free demo tests against a public automation site + live: Live Seznam tests, need a dedicated test account (run manually) diff --git a/requirements.txt b/requirements.txt index 5f98ea3..c4fd604 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ pytest==9.0.3 pytest-bdd==8.1.0 -playwright==1.42.0 -python-dotenv==1.0.1 \ No newline at end of file +playwright==1.64.1 +python-dotenv==1.2.2 \ No newline at end of file diff --git a/tests/features/demo_the_internet.feature b/tests/features/demo_the_internet.feature new file mode 100644 index 0000000..a2e7a20 --- /dev/null +++ b/tests/features/demo_the_internet.feature @@ -0,0 +1,26 @@ +Feature: Demo suite on the-internet.herokuapp.com + + @demo + Scenario: Successful login with the published demo credentials + Given the user opens the demo login page + When the user logs in with the published demo credentials + Then the secure area is displayed + + @demo + Scenario: Invalid password shows an error + Given the user opens the demo login page + When the user logs in with a wrong password + Then an invalid password error is displayed + + @demo + Scenario: Uploaded file name is shown + Given the user opens the demo upload page + When the user uploads a file named "demo_upload.txt" + Then the uploaded file name "demo_upload.txt" is displayed + + @demo + Scenario: Logout returns to the login page + Given the user opens the demo login page + And the user logs in with the published demo credentials + When the user logs out + Then the login page shows a logged out message diff --git a/tests/features/email_workflow.feature b/tests/features/email_workflow.feature index 3a93596..0651af5 100644 --- a/tests/features/email_workflow.feature +++ b/tests/features/email_workflow.feature @@ -1,6 +1,6 @@ Feature: Email Client Workflows - @level3 @smoke + @level3 @smoke @live Scenario: Send an email with an attachment to a saved contact Given the user is logged into their email account When the user composes a new email for a contact from the list diff --git a/tests/step_definitions/test_demo_steps.py b/tests/step_definitions/test_demo_steps.py new file mode 100644 index 0000000..841e77c --- /dev/null +++ b/tests/step_definitions/test_demo_steps.py @@ -0,0 +1,83 @@ +import pytest +from playwright.sync_api import expect +from pytest_bdd import scenarios, given, when, then, parsers + +from pages.the_internet_login_page import TheInternetLoginPage +from pages.the_internet_secure_page import TheInternetSecurePage +from pages.the_internet_upload_page import TheInternetUploadPage + +scenarios("demo_the_internet.feature") + +# Demo credentials are published on https://the-internet.herokuapp.com/login +DEMO_USER = "tomsmith" +DEMO_PASSWORD = "SuperSecretPassword!" + + +@pytest.fixture +def demo_login_page(page): + return TheInternetLoginPage(page) + + +@pytest.fixture +def demo_secure_page(page): + return TheInternetSecurePage(page) + + +@pytest.fixture +def demo_upload_page(page): + return TheInternetUploadPage(page) + + +@given("the user opens the demo login page") +def open_login(demo_login_page): + demo_login_page.open() + + +@given("the user opens the demo upload page") +def open_upload(demo_upload_page): + demo_upload_page.open() + + +@given("the user logs in with the published demo credentials") +@when("the user logs in with the published demo credentials") +def login_ok(demo_login_page): + demo_login_page.login(DEMO_USER, DEMO_PASSWORD) + + +@when("the user logs in with a wrong password") +def login_bad(demo_login_page): + demo_login_page.login(DEMO_USER, "wrong-password") + + +@when(parsers.parse('the user uploads a file named "{name}"')) +def upload_file(demo_upload_page, tmp_path, name): + file_path = tmp_path / name + file_path.write_text("demo upload content", encoding="utf-8") + demo_upload_page.upload(file_path) + + +@when("the user logs out") +def logout(demo_secure_page): + demo_secure_page.logout() + + +@then("the secure area is displayed") +def secure_area(page, demo_secure_page): + expect(page).to_have_url(r"https://the-internet.herokuapp.com/secure") + expect(demo_secure_page.heading()).to_contain_text("Secure Area") + + +@then("an invalid password error is displayed") +def invalid_password(demo_login_page): + expect(demo_login_page.flash_message()).to_contain_text("Your password is invalid!") + + +@then(parsers.parse('the uploaded file name "{name}" is displayed')) +def uploaded_name(demo_upload_page, name): + expect(demo_upload_page.uploaded_files()).to_have_text(name) + + +@then("the login page shows a logged out message") +def logged_out(page, demo_login_page): + expect(page).to_have_url(r"https://the-internet.herokuapp.com/login") + expect(demo_login_page.flash_message()).to_contain_text("You logged out of the secure area!") diff --git a/tests/step_definitions/test_email_steps.py b/tests/step_definitions/test_email_steps.py index ed7bbe5..7c560c5 100644 --- a/tests/step_definitions/test_email_steps.py +++ b/tests/step_definitions/test_email_steps.py @@ -14,7 +14,7 @@ def log_in(login_page): @when("the user composes a new email for a contact from the list") def compose_email(mail_page): - mail_page.compose_email_to("annea.test.milos@seznam.cz") + mail_page.compose_email_to(os.getenv("TEST_EMAIL")) @when("the user attaches a document to the email") def attach_document(mail_page):