From a4064401264a8f3d01e1c30de7f81b07acc86055 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Thu, 16 Oct 2025 22:46:49 +0200 Subject: [PATCH] refactoring integration tests --- .gitignore | 1 + Makefile | 3 + src/config.py | 4 + src/core/settings_management.py | 15 +- src/main.py | 19 ++- tests/conftest.py | 224 ++++++++++++++------------- tests/e2e/Readme.md | 122 +++++++-------- tests/e2e/e2e_constants.py | 4 + tests/e2e/e2e_helper.py | 94 +++++++++++ tests/e2e/test_e2e_authentication.py | 61 ++------ 10 files changed, 314 insertions(+), 233 deletions(-) create mode 100644 tests/e2e/e2e_constants.py create mode 100644 tests/e2e/e2e_helper.py diff --git a/.gitignore b/.gitignore index ba91596..4d06a41 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ tests/TestDBEngineRoot tests/*.png src/*.png tests/*.html +tests/*.txt test-results .sesskey tools.db diff --git a/Makefile b/Makefile index a98b7ce..1a6ef11 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,9 @@ clean: rm -rf src/tools.db rm -rf src/*.out rm -rf src/*.prof + rm -rf tests/debug*.txt + rm -rf tests/debug*.html + rm -rf tests/debug*.png find . -name '.sesskey' -exec rm -rf {} + find . -name '.pytest_cache' -exec rm -rf {} + find . -name '__pycache__' -exec rm -rf {} + diff --git a/src/config.py b/src/config.py index a526112..9e27248 100644 --- a/src/config.py +++ b/src/config.py @@ -13,6 +13,10 @@ load_dotenv() DB_PATH = os.getenv("DB_PATH", "tools.db") logger.info(f"{DB_PATH=}") +# Custom database engine settings +DBENGINE_PATH = os.getenv("DBENGINE_PATH", ".mytools_db") +logger.info(f"{DBENGINE_PATH=}") + # Authentication settings SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production") diff --git a/src/core/settings_management.py b/src/core/settings_management.py index 943043c..6a3d50b 100644 --- a/src/core/settings_management.py +++ b/src/core/settings_management.py @@ -1,6 +1,7 @@ import logging from datetime import datetime +import config from constants import NOT_LOGGED, NO_SESSION from core.dbengine import DbEngine, TAG_PARENT, TAG_USER, TAG_DATE, DbException from core.settings_objects import * @@ -92,7 +93,7 @@ class MemoryDbEngine: class SettingsManager: def __init__(self, engine=None): - self._db_engine = engine or DbEngine() + self._db_engine = engine or DbEngine(config.DBENGINE_PATH) def save(self, session: dict, entry: str, obj: object): user_id, user_email = self._get_user(session) @@ -263,3 +264,15 @@ class NestedSettingsManager: raise AttributeError(f"Settings '{self._obj_entry}' has no attribute '{self._obj_attribute}'.") return settings, getattr(settings, self._obj_attribute) + + +_settings_manager = SettingsManager() + + +def set_settings_manager(_setting_manager): + global _settings_manager + _settings_manager = _setting_manager + + +def get_settings_manager(): + return _settings_manager diff --git a/src/main.py b/src/main.py index 245e1c2..79a60c6 100644 --- a/src/main.py +++ b/src/main.py @@ -20,10 +20,10 @@ from components.register.constants import ROUTE_ROOT as REGISTER_ROUTE_ROOT from components.register.constants import Routes as RegisterRoutes from config import APP_PORT from constants import Routes -from core.dbengine import DbException from core.database_manager import initialize_database +from core.dbengine import DbException from core.instance_manager import InstanceManager -from core.settings_management import SettingsManager +from core.settings_management import get_settings_manager from pages.admin_import_settings import AdminImportSettings, IMPORT_SETTINGS_PATH, import_settings_app from pages.another_grid import get_datagrid2 from pages.basic_test import BASIC_TEST_PATH, basic_test_app, get_basic_test @@ -236,9 +236,7 @@ async def timing_middleware(request, call_next): return response -settings_manager = SettingsManager() - -import_settings = AdminImportSettings(settings_manager, None) +import_settings = AdminImportSettings(get_settings_manager(), None) pages = [ Page("My table", get_datagrid, id="my_table"), Page("new settings", import_settings, id="import_settings"), @@ -247,8 +245,8 @@ pages = [ Page("Another Table", get_datagrid2, id="another_table"), ] -login = Login(settings_manager) -register = Register(settings_manager) +login = Login(get_settings_manager()) +register = Register(get_settings_manager()) InstanceManager.register_many(login, register) @@ -258,8 +256,8 @@ def get(session): main = InstanceManager.get(session, DrawerLayout.create_component_id(session), DrawerLayout, - settings_manager=settings_manager) - return page_layout_lite(session, settings_manager, main) + settings_manager=get_settings_manager()) + return page_layout_lite(session, get_settings_manager(), main) except DbException: return RedirectResponse(LOGIN_ROUTE_ROOT + LoginRoutes.Logout, status_code=303) @@ -303,7 +301,7 @@ def not_found(path: str, session=None): return page_layout_new( session=session, - settings_manager=settings_manager, + settings_manager=get_settings_manager(), content=error_content ) @@ -317,6 +315,7 @@ def main(port): logger.info(f" Starting FastHTML server on http://localhost:{port}") serve(port=port) + if __name__ == "__main__": # Start your application logger.info("Application starting...") diff --git a/tests/conftest.py b/tests/conftest.py index 9ca35cc..5028abc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,15 @@ -from io import BytesIO - +import os +import shutil import subprocess +import sys +import tempfile +import time +from io import BytesIO +from pathlib import Path + 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 @@ -18,110 +20,124 @@ USER_EMAIL = "test@mail.com" USER_ID = "test_user" APP_PORT = 5002 + @pytest.fixture(scope="session") def test_database(): - """Configure temporary database for tests""" - # Create temporary DB - temp_db_path = TestDatabaseManager.create_temp_db_path() - - # Save original environment - original_env = { - "DB_PATH": os.environ.get("DB_PATH"), - "ADMIN_EMAIL": os.environ.get("ADMIN_EMAIL"), - "ADMIN_PASSWORD": os.environ.get("ADMIN_PASSWORD"), - "SECRET_KEY": os.environ.get("SECRET_KEY") - } - - # Configure test environment - TestDatabaseManager.setup_test_environment(temp_db_path) - - print(f"Test database created at: {temp_db_path}") - - yield temp_db_path - - # Cleanup: restore environment and clean up DB - TestDatabaseManager.restore_original_environment(original_env) - TestDatabaseManager.cleanup_test_db(temp_db_path) - print(f"Test database cleaned up: {temp_db_path}") - - -@pytest.fixture(scope="session") -def test_users(): - """Define available test users""" - return { - "admin": TestUsers.ADMIN, - "regular_user": TestUsers.REGULAR_USER, - "invalid_cases": TestUsers.INVALID_CREDENTIALS, - "protected_urls": TestUsers.PROTECTED_URLS - } + """Configure temporary database for tests""" + # Create temporary DB + temp_db_path = TestDatabaseManager.create_temp_db_path() + + # Save original environment + original_env = { + "DB_PATH": os.environ.get("DB_PATH"), + "ADMIN_EMAIL": os.environ.get("ADMIN_EMAIL"), + "ADMIN_PASSWORD": os.environ.get("ADMIN_PASSWORD"), + "SECRET_KEY": os.environ.get("SECRET_KEY") + } + + # Configure test environment + TestDatabaseManager.setup_test_environment(temp_db_path) + + print(f"Test database created at: {temp_db_path}") + + yield temp_db_path + + # Cleanup: restore environment and clean up DB + TestDatabaseManager.restore_original_environment(original_env) + TestDatabaseManager.cleanup_test_db(temp_db_path) + print(f"Test database cleaned up: {temp_db_path}") @pytest.fixture(scope="session") -def app_server(test_database, test_users): - """Start application server with test database""" - # 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" - - # Create test environment - test_env = os.environ.copy() - test_env["DB_PATH"] = test_database - test_env["ADMIN_EMAIL"] = test_users["admin"]["email"] - test_env["ADMIN_PASSWORD"] = test_users["admin"]["password"] - test_env["SECRET_KEY"] = "test-secret-key-for-e2e-tests" - - # Start the application server - print(f"Starting server on url {BASE_URL} with test database...") - port = BASE_URL.split(':')[-1].split('/')[0] if ':' in BASE_URL else APP_PORT - print(f"Using port {port}") - print(f"Test DB path: {test_database}") - - 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=test_env # Use test environment - ) - - # 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('Test server started with isolated database!') - yield server_process - - # Cleanup: terminate the server after tests - server_process.terminate() +def test_users(): + """Define available test users""" + return { + "admin": TestUsers.ADMIN, + "regular_user": TestUsers.REGULAR_USER, + "invalid_cases": TestUsers.INVALID_CREDENTIALS, + "protected_urls": TestUsers.PROTECTED_URLS + } + + +@pytest.fixture(scope="session") +def test_setting_manager(): + # create a dedicated folder for the db + db_engine_folder = tempfile.mkdtemp(prefix="test_db_engine") + + yield db_engine_folder + + # clean up folder & restore settings manager + if os.path.exists(db_engine_folder): + shutil.rmtree(db_engine_folder, ignore_errors=True) + + +@pytest.fixture(scope="session") +def app_server(test_database, test_users, test_setting_manager): + """Start application server with test database""" + # 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" + + # Create test environment + test_env = os.environ.copy() + test_env["DB_PATH"] = test_database + test_env["DBENGINE_PATH"] = test_setting_manager + test_env["ADMIN_EMAIL"] = test_users["admin"]["email"] + test_env["ADMIN_PASSWORD"] = test_users["admin"]["password"] + test_env["SECRET_KEY"] = "test-secret-key-for-e2e-tests" + + # Start the application server + print(f"Starting server on url {BASE_URL} with test database...") + port = BASE_URL.split(':')[-1].split('/')[0] if ':' in BASE_URL else APP_PORT + print(f"Using port {port}") + print(f"Test DB path: {test_database}") + + 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=test_env # Use test environment + ) + + # Wait for the server to start + max_retries = 10 # Wait up to 30 seconds + for i in range(max_retries): try: - server_process.wait(timeout=5) - print('Test server stopped.') - except subprocess.TimeoutExpired: - server_process.kill() - server_process.wait() - print('Test server killed!') + 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('Test server started with isolated database!') + yield server_process + + # Cleanup: terminate the server after tests + server_process.terminate() + try: + server_process.wait(timeout=5) + print('Test server stopped.') + except subprocess.TimeoutExpired: + server_process.kill() + server_process.wait() + print('Test server killed!') @pytest.fixture(scope="session") diff --git a/tests/e2e/Readme.md b/tests/e2e/Readme.md index e1d6d1f..443ee85 100644 --- a/tests/e2e/Readme.md +++ b/tests/e2e/Readme.md @@ -1,18 +1,21 @@ # README - End-to-End Authentication Testing Guide -This README provides details on how to set up, configure, and run the end-to-end (e2e) authentication tests for the **My Managing Tools** application. The tests are implemented using `pytest` with `playwright` for browser automation. +This README provides details on how to set up, configure, and run the end-to-end (e2e) authentication tests for the **My +Managing Tools** application. The tests are implemented using `pytest` with `playwright` for browser automation. ## Purpose -The e2e tests verify that the authentication system works as expected, including login functionality, session validation, and database isolation for testing purposes. These tests ensure the login page behaves properly under different scenarios such as unauthenticated access, form validation, and successful user login. +The e2e tests verify that the authentication system works as expected, including login functionality, session +validation, and database isolation for testing purposes. These tests ensure the login page behaves properly under +different scenarios such as unauthenticated access, form validation, and successful user login. --- ## Table of Contents 1. [Setup Instructions](#setup-instructions) - - [Application Setup](#application-setup) - - [Database Setup](#database-setup) + - [Application Setup](#application-setup) + - [Database Setup](#database-setup) 2. [Installing Dependencies](#installing-dependencies) 3. [Running Tests](#running-tests) 4. [Debugging and Logs](#debugging-and-logs) @@ -21,52 +24,6 @@ The e2e tests verify that the authentication system works as expected, including --- -## Setup Instructions - -Before running the tests, make sure the environment is set up correctly for both the application and the test database. - -### Application Setup - -1. **Install Required Dependencies** (Python version 3.9 or higher is recommended): - ```bash - pip install -r requirements.txt - ``` - -2. **Run the Application**: - Navigate to the `src` directory and start the application: - ```bash - cd src - python main.py - ``` - Alternatively, use Docker: - ```bash - docker-compose up -d - ``` - - Once the server starts, ensure the app is accessible at the base URL defined in `playwright_config.py`. By default, it should be `http://localhost:5002`. - -### Database Setup - -The e2e tests are designed to interact with an **isolated test database**. During setup, a temporary SQLite database will be created for each test session. - -1. **Test Database Initialization**: - The database is initialized dynamically during the test execution using a custom temporary path. The `test_database` fixture creates a database with the following characteristics: - - **Temporary Directory**: Ensures no conflicts with the production database. - - **Admin User Auto-Creation**: Adds an admin account if the environment variables `ADMIN_EMAIL` and `ADMIN_PASSWORD` are set. - -2. **Environment Variables**: - Ensure the following environment variables are defined for the test database: - ```env - DB_PATH="test_mmt_tools.db" - ADMIN_EMAIL="test.admin@test.com" - ADMIN_PASSWORD="TestAdmin123" - ``` - -3. **Configuration**: - The `test_database.py` script handles the database isolation and cleanup after each test session. It ensures that no state is leaked between tests. - ---- - ## Installing Dependencies Install the required packages for the tests: @@ -93,6 +50,29 @@ Install the required packages for the tests: --- + +### Database Setup + +The e2e tests are designed to interact with an **isolated test database**. During setup, a temporary SQLite database will be created for each test session. + +1. **Test Database Initialization**: + The database is initialized dynamically during the test execution using a custom temporary path. The `test_database` fixture creates a database with the following characteristics: + - **Temporary Directory**: Ensures no conflicts with the production database. + - **Admin User Auto-Creation**: Adds an admin account if the environment variables `ADMIN_EMAIL` and `ADMIN_PASSWORD` are set. + +2. **Environment Variables**: + Ensure the following environment variables are defined for the test database: + ```env + DB_PATH="test_mmt_tools.db" + ADMIN_EMAIL="test.admin@test.com" + ADMIN_PASSWORD="TestAdmin123" + ``` + +3. **Configuration**: + The `test_database.py` script handles the database isolation and cleanup after each test session. It ensures that no state is leaked between tests. + +--- + ## Running Tests To execute the end-to-end authentication tests, run the following commands from the project root. @@ -126,22 +106,35 @@ To execute the end-to-end authentication tests, run the following commands from ## Debugging and Logs 1. **Console Logs**: - Console errors during page navigation are captured using the `test_page_loads_without_errors` test. Check the pytest output for details. + Console errors during page navigation are captured using the `test_page_loads_without_errors` test. Check the pytest + output for details. 2. **Debugging Tools**: - - Videos and HAR traces are recorded in the `test-results` directory. - - Use the `page.pause()` method inside a test for interactive debugging with Playwright Inspector. + - Videos and HAR traces are recorded in the `test-results` directory. + - Use the `page.pause()` method inside a test for interactive debugging with Playwright Inspector. 3. **Screenshots and Debug HTML**: If a test fails, screenshots and full HTML are saved for post-mortem debugging: - - `debug_after_login.png`: Screenshot post-login. - - `debug_page.html`: Captured HTML structure of the page. + - `debug_after_login.png`: Screenshot post-login. + - `debug_page.html`: Captured HTML structure of the page. -4. **Server Logs**: +4. **Manual Debugging**: + ```python + # Screenshot + page.screenshot(path="debug_after_login.png") + + # HTML content + with open("debug_page.html", "w", encoding="utf-8") as f: + f.write(page.content()) + ``` + +5. **Server Logs**: When using the `app_server` fixture, server `stdout` and `stderr` are recorded during the test execution. --- + + ## Test Scenarios The main authentication tests cover the following scenarios: @@ -168,20 +161,17 @@ For a full list of test cases, see `tests/test_e2e_authentication.py`. ## Troubleshooting - **Server Not Starting**: - If the server fails to start during testing, confirm the `DB_PATH`, `BASE_URL`, and all dependencies are correctly configured in the `.env` file. + If the server fails to start during testing, confirm the `DB_PATH`, `BASE_URL`, and all dependencies are correctly + configured in the `.env` file. - **Playwright Issues**: - Ensure Playwright dependencies (`playwright install`) are installed and functional. Missing browsers or outdated Playwright versions can cause tests to fail. + Ensure Playwright dependencies (`playwright install`) are installed and functional. Missing browsers or outdated + Playwright versions can cause tests to fail. - **Database Error**: - Check that the `tools.db` file or the temporary test database exists and is accessible. Ensure the test database environment variables are set correctly during tests. + Check that the `tools.db` file or the temporary test database exists and is accessible. Ensure the test database + environment variables are set correctly during tests. --- -## Additional Notes - -- The app server is started with test-specific configurations (`app_server` fixture) that ensure end-to-end isolation. -- Tests requiring authentication use an admin user defined in the `test_users` fixture: - - Email: `test.admin@test.com` - - Password: `TestAdmin123` -- Ensure no sensitive data or configurations are hardcoded in the test scripts. \ No newline at end of file +igurations are hardcoded in the test scripts. \ No newline at end of file diff --git a/tests/e2e/e2e_constants.py b/tests/e2e/e2e_constants.py new file mode 100644 index 0000000..145ca99 --- /dev/null +++ b/tests/e2e/e2e_constants.py @@ -0,0 +1,4 @@ +class Login: + email = ["input[type='email']", "input[name='email']"] + password = ["input[type='password']", "input[name='password']"] + submit = "button[type='submit']" diff --git a/tests/e2e/e2e_helper.py b/tests/e2e/e2e_helper.py new file mode 100644 index 0000000..51d9456 --- /dev/null +++ b/tests/e2e/e2e_helper.py @@ -0,0 +1,94 @@ +from playwright.sync_api import Page, expect + + +def locate(page: Page, name, check_visible=True): + """ + Locate an element or elements on the provided page by name. + + This function identifies elements on a web page using the locator API of the + provided page object. The `name` parameter can either be a single locator + string or a list of locator strings. If a list is provided, its elements are + joined into a single comma-separated string before locating the elements. + + Args: + page (Page): The web page object in which to locate the element(s). + name (Union[str, List[str]]): Locator or list of locators to identify elements. + check_visible(bool): Whether to check if the element is visible before returning it. + + Returns: + Locator: The locator object for the given name. + """ + if isinstance(name, list): + name = ",".join(name) + + element = page.locator(name) + + if check_visible: + expect(element).to_be_visible() + + return element + + +def fill(element, value): + """ + Fills an element with a specified value and verifies that the element's value matches the provided value. + + Parameters: + element : The UI element to be filled with the value. + value : The value to fill into the element. + + Raises: + AssertionError: If the element's value does not match the provided value after the fill operation. + """ + element.fill(value) + expect(element).to_have_value(value) + + +def debug(page: Page, checkpoint_name: str): + """ + Takes a screenshot of the current state of a page and saves its full HTML content + to support debugging at a specified checkpoint. The screenshot and HTML file are saved + with checkpoint-specific filenames in the current working directory. + + This function is useful for capturing the visual state and content of the page at + any moment during the script execution, helping in resolving issues or verifying + state transitions. + + Arguments: + page (Page): The page object whose state and content are to be captured. + checkpoint_name (str): A descriptive name for the checkpoint. This is used to + generate unique filenames for the screenshot and HTML file. + """ + # Take screenshot + page.screenshot(path=f"debug_{checkpoint_name}.png") + + # Save full HTML for inspection + with open(f"debug_{checkpoint_name}.html", "w", encoding="utf-8") as f: + f.write(page.content()) + + with open(f"debug_{checkpoint_name}.txt", "w", encoding="utf-8") as f: + f.writelines([ + f"Checkpoint: {checkpoint_name}\n", + f"URL: {page.url}\n", + f"Title: {page.title()}\n", + ]) + + +def check_not_in_content(page, keywords): + """ + Checks that none of the specified keywords are present in the page content. + + This function iterates through a list of keywords and verifies that none of + them are present in the given page's content. If any keyword is found, an + AssertionError is raised with details about the number of times it was found. + + Args: + page: A page object that provides access to retrieve text content. + keywords: List of strings representing keywords to search in the page content. + + Raises: + AssertionError: If any of the keywords are found in the page content. + """ + for keyword in keywords: + occurrences = page.get_by_text(keyword, exact=False) + assert occurrences.count() == 0, f"Found {occurrences.count()} of '{keyword}' in page content which should not be visible" diff --git a/tests/e2e/test_e2e_authentication.py b/tests/e2e/test_e2e_authentication.py index fb1be90..cbea7e2 100644 --- a/tests/e2e/test_e2e_authentication.py +++ b/tests/e2e/test_e2e_authentication.py @@ -3,6 +3,8 @@ import pytest from playwright.sync_api import Page, expect +from e2e.e2e_constants import Login +from e2e.e2e_helper import locate, fill, debug, check_not_in_content from playwright_config import BASE_URL @@ -149,63 +151,18 @@ class TestAuthentication: expect(page).to_have_url(f"{BASE_URL}/authlogin/login") # Step 2: Locate form elements - email_input = page.locator("input[type='email'], input[name='email']") - password_input = page.locator("input[type='password'], input[name='password']") - submit_button = page.locator("button[type='submit']") + email_input = locate(page, Login.email) + password_input = locate(page, Login.password) + submit_button = locate(page, Login.submit) - # Verify all required elements are present and visible - expect(email_input).to_be_visible() - expect(password_input).to_be_visible() - expect(submit_button).to_be_visible() + fill(email_input, admin_user["email"]) + fill(password_input, admin_user["password"]) - # Step 3: Fill in valid credentials - email_input.fill(admin_user["email"]) - password_input.fill(admin_user["password"]) - - # Verify credentials were entered correctly - expect(email_input).to_have_value(admin_user["email"]) - expect(password_input).to_have_value(admin_user["password"]) - - # Step 4: Submit the login form - # Use click with wait for navigation to handle the redirect with page.expect_navigation(wait_until="networkidle"): submit_button.click() - # DEBUGGING BLOCK - Add this - print(f"🔍 Current URL: {page.url}") - print(f"🔍 Page title: {page.title()}") - - # Take screenshot - page.screenshot(path="debug_after_login.png") - - # Save full HTML for inspection - with open("debug_page.html", "w", encoding="utf-8") as f: - f.write(page.content()) - - # Check specific content that's causing the failure - page_content = page.content().lower() - login_keywords = ["sign in", "login", "authenticate"] - - print("🔍 Checking for login keywords:") - for keyword in login_keywords: - if keyword in page_content: - print(f" ❌ Found '{keyword}' in page content") - # Find exactly where this keyword appears - occurrences = page.get_by_text(keyword, exact=False) - count = occurrences.count() - print(f" Found in {count} elements:") - for i in range(min(count, 3)): # Show first 3 occurrences - element = occurrences.nth(i) - try: - print(f" - Element {i}: '{element.inner_text()[:50]}...' (visible: {element.is_visible()})") - except: - print(f" - Element {i}: Could not get text") - else: - print(f" ✅ '{keyword}' NOT found") - - # Your original assertion (will still fail, but now you have debug info) - has_login_content = any(keyword in page_content for keyword in login_keywords) - assert not has_login_content, "Login content still visible after successful authentication" + debug(page, "after_login") + check_not_in_content(page, ["sign in", "login", "authenticate"]) page.pause()