From db56363b1fecd4edc0a76b400e792c35f3694837 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Sat, 30 Aug 2025 18:51:42 +0200 Subject: [PATCH] Working version of playwright --- .gitignore | 1 + .idea/inspectionProfiles/Project_Default.xml | 6 -- Makefile | 28 ++++- README.md | 16 ++- pytest.ini | 11 ++ requirements.txt | 11 ++ src/main.py | 10 +- tests/conftest.py | 84 +++++++++++++++ tests/playwright_config.py | 28 +++++ tests/test_e2e_authentication.py | 103 +++++++++++++++++++ 10 files changed, 286 insertions(+), 12 deletions(-) delete mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 pytest.ini create mode 100644 tests/playwright_config.py create mode 100644 tests/test_e2e_authentication.py diff --git a/.gitignore b/.gitignore index 3ac9816..242b4a2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ htmlcov .venv tests/settings_from_unit_testing.json tests/TestDBEngineRoot +test-results .sesskey tools.db .mytools_db diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 4069cea..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/Makefile b/Makefile index e885a8a..2133d13 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ -.PHONY: test +.PHONY: test install-playwright install-playwright-deps test-e2e test-e2e-headed test-regression + test: pytest @@ -7,6 +8,31 @@ coverage: coverage run --source=src -m pytest coverage html +install-playwright: + @echo "Installing Playwright and pytest-playwright via pip..." + pip install playwright pytest-playwright + @echo "Installing Playwright browsers..." + playwright install + @echo "Installing system dependencies for Ubuntu/WSL2..." + playwright install-deps + @echo "Playwright installation complete!" + +install-playwright-deps: + @echo "Installing system dependencies for Playwright on Ubuntu/WSL2..." + playwright install-deps + +test-e2e: + pytest tests/test_e2e_regression.py -m "e2e" --browser chromium + +test-e2e-headed: + pytest -m "e2e" --browser chromium --headed + +test-regression: + pytest tests/test_e2e_regression.py -m "regression" --browser chromium + +test-all-browsers: + pytest tests/test_e2e_regression.py -m "e2e" --browser chromium --browser firefox --browser webkit + clean: rm -rf build rm -rf htmlcov diff --git a/README.md b/README.md index 0eb3c4f..4a91e86 100644 --- a/README.md +++ b/README.md @@ -41,4 +41,18 @@ docker-compose build cd src python -m cProfile -o profile.out main.py snakeviz profile.out # 'pip install snakeviz' if snakeviz is not installed -``` \ No newline at end of file +``` + + +# End to end testing +```shell +make install-playwright +``` + +Alternatively, you can install Playwright and pytest-playwright via pip: +```shell +pip install playwright pytest-playwright +playwright install +playwright install-deps # may be required on Linux +playwright --version +``` diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..714567b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,11 @@ +[pytest] +python_files = test_*.py +python_classes = Test* +python_functions = test_* +testpaths = tests +pythonpath = src tests +addopts = --tb=short -v +markers = + e2e: marks tests as end-to-end tests + smoke: marks tests as smoke tests + regression: marks tests as regression tests \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b9b5316..b8ffec6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,8 @@ click==8.1.7 et-xmlfile==1.1.0 fastcore==1.8.5 fastlite==0.2.1 +gprof2dot==2025.4.14 +greenlet==3.2.4 h11==0.14.0 httpcore==1.0.5 httptools==0.6.1 @@ -26,28 +28,37 @@ oauthlib==3.2.2 openpyxl==3.1.5 packaging==24.1 pandas==2.2.3 +pandas-stubs==2.3.2.250827 +playwright==1.55.0 pluggy==1.5.0 pydantic==2.11.5 pydantic-settings==2.9.1 pydantic_core==2.33.2 +pyee==13.0.0 Pygments==2.19.1 pytest==8.3.3 +pytest-base-url==2.1.0 pytest-mock==3.14.1 +pytest-playwright==0.7.0 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 python-fasthtml==0.12.21 python-multipart==0.0.10 +python-slugify==8.0.4 pytz==2024.2 PyYAML==6.0.2 requests==2.32.3 rich==14.0.0 shellingham==1.5.4 six==1.16.0 +snakeviz==2.2.2 sniffio==1.3.1 soupsieve==2.6 sqlite-minutils==3.37.0.post3 sse-starlette==2.3.6 starlette==0.38.5 +text-unidecode==1.3 +tornado==6.5.2 typer==0.16.0 typing-inspection==0.4.1 typing_extensions==4.13.2 diff --git a/src/main.py b/src/main.py index 0649500..7175b32 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,7 @@ # global layout import logging.config +import click import yaml from fasthtml.common import * @@ -306,10 +307,11 @@ def not_found(path: str, session=None): setup_toasts(app) -def main(): - logger.info(f" Starting FastHTML server on http://localhost:{APP_PORT}") - serve(port=APP_PORT) - +@click.command() +@click.option('--port', default=APP_PORT, help='Port to run the server on') +def main(port): + logger.info(f" Starting FastHTML server on http://localhost:{port}") + serve(port=port) if __name__ == "__main__": # Start your application diff --git a/tests/conftest.py b/tests/conftest.py index 5dd9809..a322074 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,96 @@ from io import BytesIO +import subprocess import pandas as pd import pytest +import requests +import time +import sys +import os +from pathlib import Path from components.datagrid.DataGrid import reset_instances +from playwright_config import BROWSER_CONFIG, BASE_URL USER_EMAIL = "test@mail.com" USER_ID = "test_user" +APP_PORT = 5002 + +@pytest.fixture(scope="session") +def app_server(): + """Start the application server for end-to-end tests""" + # Use the same Python executable that's running pytest + python_executable = sys.executable + + # Get the absolute path to the src directory + project_root = Path(__file__).parent.parent + src_path = project_root / "src" + + # Start the application server + print(f"Starting server on url {BASE_URL}...") + port = BASE_URL.split(':')[-1].split('/')[0] if ':' in BASE_URL else APP_PORT + print(f"Using port {port}") + + server_process = subprocess.Popen( + [python_executable, "main.py", "--port", "5002"], + cwd=str(src_path), # Change to src directory where main.py is located + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=os.environ.copy() # Inherit environment variables + ) + + # Wait for the server to start + max_retries = 10 # Wait up to 30 seconds + for i in range(max_retries): + try: + print(f"Waiting retry {i}/{max_retries}") + response = requests.get(BASE_URL, timeout=1) + if response.status_code in [200, 302, 404]: # Server is responding + print(f"Server started successfully after {i + 1} attempts") + break + except requests.exceptions.RequestException: + time.sleep(1) + else: + # If we get here, the server didn't start in time + print(f"Failed to start after {max_retries} attempts.") + server_process.kill() + stdout, stderr = server_process.communicate() + raise RuntimeError( + f"Server failed to start within {max_retries} seconds.\n" + f"STDOUT: {stdout.decode()}\n" + f"STDERR: {stderr.decode()}" + ) + + # Yield control to the tests + print('Server started !') + yield server_process + + # Cleanup: terminate the server after tests + server_process.terminate() + try: + server_process.wait(timeout=5) + print('Server stopped.') + except subprocess.TimeoutExpired: + server_process.kill() + server_process.wait() + print('Server killed !') + + +@pytest.fixture(scope="session") +def browser_context_args(browser_context_args): + """Configure browser context arguments""" + return { + **browser_context_args, + **BROWSER_CONFIG, + "record_video_dir": "test-results/videos/", + "record_har_path": "test-results/har/trace.har", + } + + +@pytest.fixture(scope="session") +def app_url(): + """Base URL for the application""" + return BASE_URL @pytest.fixture diff --git a/tests/playwright_config.py b/tests/playwright_config.py new file mode 100644 index 0000000..960a93f --- /dev/null +++ b/tests/playwright_config.py @@ -0,0 +1,28 @@ +import os +from playwright.sync_api import Playwright + +# Playwright configuration +BASE_URL = os.getenv("APP_URL", "http://localhost:5002") +TIMEOUT = 30000 +EXPECT_TIMEOUT = 5000 + +def pytest_configure(): + """Configure pytest for Playwright""" + pass + +# Browser configuration +BROWSER_CONFIG = { + #"headless": os.getenv("HEADLESS", "true").lower() == "true", + "viewport": {"width": 1280, "height": 720}, + "ignore_https_errors": True, +} + +# Test configuration +TEST_CONFIG = { + "base_url": BASE_URL, + "timeout": TIMEOUT, + "expect_timeout": EXPECT_TIMEOUT, + "screenshot": "only-on-failure", + "video": "retain-on-failure", + "trace": "retain-on-failure", +} \ No newline at end of file diff --git a/tests/test_e2e_authentication.py b/tests/test_e2e_authentication.py new file mode 100644 index 0000000..c0b4267 --- /dev/null +++ b/tests/test_e2e_authentication.py @@ -0,0 +1,103 @@ +# tests/test_e2e_authentication.py + +import pytest +from playwright.sync_api import Page, expect + +from playwright_config import BASE_URL + + +class TestAuthentication: + """Tests for authentication and login functionality""" + + @pytest.mark.e2e + @pytest.mark.smoke + def test_unauthenticated_user_redirected_to_login(self, app_server, page: Page): + """Test that when not logged in, the default page is the login page""" + # Navigate to the root URL + page.goto(BASE_URL) + + # Wait for the page to fully load + page.wait_for_load_state("networkidle") + + # Check that we're on the login page + # Option 1: Check URL contains login-related path + expect(page).to_have_url(f"{BASE_URL}/authlogin/login") + + # Option 2: Check for login form elements + # Look for typical login form elements + login_indicators = [ + "input[type='email']", + "input[type='password']", + "input[name*='username']", + "input[name*='email']", + "input[name*='password']", + "button[type='submit']", + "form" + ] + + # At least one login indicator should be present + login_form_found = False + for selector in login_indicators: + if page.locator(selector).count() > 0: + login_form_found = True + break + + assert login_form_found, "No login form elements found on the page" + + # Option 3: Check for login-related text content + page_content = page.content().lower() + login_keywords = ["login", "sign in", "authenticate", "username", "password", "email"] + + has_login_content = any(keyword in page_content for keyword in login_keywords) + assert has_login_content, "Page does not contain login-related content" + + # Option 4: Ensure we're not on a protected page + # Check that we don't see protected content like "dashboard", "logout", etc. + protected_content = ["dashboard", "logout", "profile", "settings"] + page_text = page.locator("body").inner_text().lower() + + for protected_word in protected_content: + assert protected_word not in page_text, f"Found protected content '{protected_word}' when not logged in" + + @pytest.mark.e2e + @pytest.mark.regression + def test_login_page_has_required_elements(self, page: Page): + """Test that the login page contains all required elements""" + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + + # Check for essential login form elements + expect(page.locator("form")).to_be_visible() + + # Check for input fields (at least email/username and password) + username_input = page.locator("input[type='email'], input[name*='username'], input[name*='email']") + expect(username_input.first).to_be_visible() + + password_input = page.locator("input[type='password'], input[name*='password']") + expect(password_input.first).to_be_visible() + + # Check for submit button + submit_button = page.locator("button[type='submit'], input[type='submit']") + expect(submit_button.first).to_be_visible() + + @pytest.mark.e2e + def test_page_loads_without_errors(self, page: Page): + """Test that the login page loads without console errors""" + console_errors = [] + + def handle_console(msg): + if msg.type == "error": + console_errors.append(msg.text) + + page.on("console", handle_console) + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + + # Check for console errors + assert len(console_errors) == 0, f"Console errors found: {console_errors}" + + # Check that the page actually loaded (not a 404 or 500 error) + expect(page.locator("body")).to_be_visible() + + # Check that the page has a title + expect(page).to_have_title("My Managing Tools")