Working version of playwright

This commit is contained in:
2025-08-30 18:51:42 +02:00
parent 3bd503d4d2
commit db56363b1f
10 changed files with 286 additions and 12 deletions

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@ htmlcov
.venv .venv
tests/settings_from_unit_testing.json tests/settings_from_unit_testing.json
tests/TestDBEngineRoot tests/TestDBEngineRoot
test-results
.sesskey .sesskey
tools.db tools.db
.mytools_db .mytools_db

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyInitNewSignatureInspection" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

View File

@@ -1,4 +1,5 @@
.PHONY: test .PHONY: test install-playwright install-playwright-deps test-e2e test-e2e-headed test-regression
test: test:
pytest pytest
@@ -7,6 +8,31 @@ coverage:
coverage run --source=src -m pytest coverage run --source=src -m pytest
coverage html 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: clean:
rm -rf build rm -rf build
rm -rf htmlcov rm -rf htmlcov

View File

@@ -41,4 +41,18 @@ docker-compose build
cd src cd src
python -m cProfile -o profile.out main.py python -m cProfile -o profile.out main.py
snakeviz profile.out # 'pip install snakeviz' if snakeviz is not installed snakeviz profile.out # 'pip install snakeviz' if snakeviz is not installed
``` ```
# 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
```

11
pytest.ini Normal file
View File

@@ -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

View File

@@ -10,6 +10,8 @@ click==8.1.7
et-xmlfile==1.1.0 et-xmlfile==1.1.0
fastcore==1.8.5 fastcore==1.8.5
fastlite==0.2.1 fastlite==0.2.1
gprof2dot==2025.4.14
greenlet==3.2.4
h11==0.14.0 h11==0.14.0
httpcore==1.0.5 httpcore==1.0.5
httptools==0.6.1 httptools==0.6.1
@@ -26,28 +28,37 @@ oauthlib==3.2.2
openpyxl==3.1.5 openpyxl==3.1.5
packaging==24.1 packaging==24.1
pandas==2.2.3 pandas==2.2.3
pandas-stubs==2.3.2.250827
playwright==1.55.0
pluggy==1.5.0 pluggy==1.5.0
pydantic==2.11.5 pydantic==2.11.5
pydantic-settings==2.9.1 pydantic-settings==2.9.1
pydantic_core==2.33.2 pydantic_core==2.33.2
pyee==13.0.0
Pygments==2.19.1 Pygments==2.19.1
pytest==8.3.3 pytest==8.3.3
pytest-base-url==2.1.0
pytest-mock==3.14.1 pytest-mock==3.14.1
pytest-playwright==0.7.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-dotenv==1.0.1 python-dotenv==1.0.1
python-fasthtml==0.12.21 python-fasthtml==0.12.21
python-multipart==0.0.10 python-multipart==0.0.10
python-slugify==8.0.4
pytz==2024.2 pytz==2024.2
PyYAML==6.0.2 PyYAML==6.0.2
requests==2.32.3 requests==2.32.3
rich==14.0.0 rich==14.0.0
shellingham==1.5.4 shellingham==1.5.4
six==1.16.0 six==1.16.0
snakeviz==2.2.2
sniffio==1.3.1 sniffio==1.3.1
soupsieve==2.6 soupsieve==2.6
sqlite-minutils==3.37.0.post3 sqlite-minutils==3.37.0.post3
sse-starlette==2.3.6 sse-starlette==2.3.6
starlette==0.38.5 starlette==0.38.5
text-unidecode==1.3
tornado==6.5.2
typer==0.16.0 typer==0.16.0
typing-inspection==0.4.1 typing-inspection==0.4.1
typing_extensions==4.13.2 typing_extensions==4.13.2

View File

@@ -1,6 +1,7 @@
# global layout # global layout
import logging.config import logging.config
import click
import yaml import yaml
from fasthtml.common import * from fasthtml.common import *
@@ -306,10 +307,11 @@ def not_found(path: str, session=None):
setup_toasts(app) setup_toasts(app)
def main(): @click.command()
logger.info(f" Starting FastHTML server on http://localhost:{APP_PORT}") @click.option('--port', default=APP_PORT, help='Port to run the server on')
serve(port=APP_PORT) def main(port):
logger.info(f" Starting FastHTML server on http://localhost:{port}")
serve(port=port)
if __name__ == "__main__": if __name__ == "__main__":
# Start your application # Start your application

View File

@@ -1,12 +1,96 @@
from io import BytesIO from io import BytesIO
import subprocess
import pandas as pd import pandas as pd
import pytest import pytest
import requests
import time
import sys
import os
from pathlib import Path
from components.datagrid.DataGrid import reset_instances from components.datagrid.DataGrid import reset_instances
from playwright_config import BROWSER_CONFIG, BASE_URL
USER_EMAIL = "test@mail.com" USER_EMAIL = "test@mail.com"
USER_ID = "test_user" 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 @pytest.fixture

View File

@@ -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",
}

View File

@@ -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")