Working version of playwright
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@ htmlcov
|
||||
.venv
|
||||
tests/settings_from_unit_testing.json
|
||||
tests/TestDBEngineRoot
|
||||
test-results
|
||||
.sesskey
|
||||
tools.db
|
||||
.mytools_db
|
||||
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
6
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -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>
|
||||
28
Makefile
28
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
|
||||
|
||||
16
README.md
16
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
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
# 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
11
pytest.ini
Normal 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
|
||||
@@ -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
|
||||
|
||||
10
src/main.py
10
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
|
||||
|
||||
@@ -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
|
||||
|
||||
28
tests/playwright_config.py
Normal file
28
tests/playwright_config.py
Normal 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",
|
||||
}
|
||||
103
tests/test_e2e_authentication.py
Normal file
103
tests/test_e2e_authentication.py
Normal 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")
|
||||
Reference in New Issue
Block a user