From 94a84dd1913a0deaa5dd787eb35f360e3c066560 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 03:40:23 +0100 Subject: [PATCH 01/20] Add interoperability tests between Python and TypeScript using Redis --- tests/interop/README.md | 94 +++++++ .../interop/python-redis-typescript/README.md | 67 +++++ .../python-redis-typescript/create_state.py | 82 ++++++ .../python-redis-typescript/package-lock.json | 13 + .../python-redis-typescript/package.json | 12 + .../python-redis-typescript/run_tests.sh | 81 ++++++ .../test_cross_language.py | 147 +++++++++++ .../python-redis-typescript/venv/.gitignore | 4 + .../venv/bin/Activate.ps1 | 247 ++++++++++++++++++ .../python-redis-typescript/venv/bin/activate | 69 +++++ .../venv/bin/activate.csh | 26 ++ .../venv/bin/activate.fish | 69 +++++ .../python-redis-typescript/venv/bin/pip | 8 + .../python-redis-typescript/venv/bin/pip3 | 8 + .../python-redis-typescript/venv/bin/pip3.11 | 8 + .../python-redis-typescript/venv/bin/python | 1 + .../python-redis-typescript/venv/bin/python3 | 1 + .../venv/bin/python3.11 | 1 + .../python-redis-typescript/venv/pyvenv.cfg | 5 + .../python-redis-typescript/verify_state.ts | 99 +++++++ tests/interop/run_all.sh | 63 +++++ tests/interop/setup.sh | 58 ++++ .../interop/typescript-redis-python/README.md | 67 +++++ .../typescript-redis-python/create_state.ts | 107 ++++++++ .../typescript-redis-python/package-lock.json | 13 + .../typescript-redis-python/package.json | 12 + .../typescript-redis-python/run_tests.sh | 81 ++++++ .../typescript-redis-python/venv/.gitignore | 4 + .../venv/bin/Activate.ps1 | 247 ++++++++++++++++++ .../typescript-redis-python/venv/bin/activate | 69 +++++ .../venv/bin/activate.csh | 26 ++ .../venv/bin/activate.fish | 69 +++++ .../typescript-redis-python/venv/bin/pip | 8 + .../typescript-redis-python/venv/bin/pip3 | 8 + .../typescript-redis-python/venv/bin/pip3.11 | 8 + .../typescript-redis-python/venv/bin/python | 1 + .../typescript-redis-python/venv/bin/python3 | 1 + .../venv/bin/python3.11 | 1 + .../typescript-redis-python/venv/pyvenv.cfg | 5 + .../typescript-redis-python/verify_state.py | 96 +++++++ 40 files changed, 1986 insertions(+) create mode 100644 tests/interop/README.md create mode 100644 tests/interop/python-redis-typescript/README.md create mode 100644 tests/interop/python-redis-typescript/create_state.py create mode 100644 tests/interop/python-redis-typescript/package-lock.json create mode 100644 tests/interop/python-redis-typescript/package.json create mode 100755 tests/interop/python-redis-typescript/run_tests.sh create mode 100644 tests/interop/python-redis-typescript/test_cross_language.py create mode 100644 tests/interop/python-redis-typescript/venv/.gitignore create mode 100644 tests/interop/python-redis-typescript/venv/bin/Activate.ps1 create mode 100644 tests/interop/python-redis-typescript/venv/bin/activate create mode 100644 tests/interop/python-redis-typescript/venv/bin/activate.csh create mode 100644 tests/interop/python-redis-typescript/venv/bin/activate.fish create mode 100755 tests/interop/python-redis-typescript/venv/bin/pip create mode 100755 tests/interop/python-redis-typescript/venv/bin/pip3 create mode 100755 tests/interop/python-redis-typescript/venv/bin/pip3.11 create mode 120000 tests/interop/python-redis-typescript/venv/bin/python create mode 120000 tests/interop/python-redis-typescript/venv/bin/python3 create mode 120000 tests/interop/python-redis-typescript/venv/bin/python3.11 create mode 100644 tests/interop/python-redis-typescript/venv/pyvenv.cfg create mode 100644 tests/interop/python-redis-typescript/verify_state.ts create mode 100755 tests/interop/run_all.sh create mode 100755 tests/interop/setup.sh create mode 100644 tests/interop/typescript-redis-python/README.md create mode 100644 tests/interop/typescript-redis-python/create_state.ts create mode 100644 tests/interop/typescript-redis-python/package-lock.json create mode 100644 tests/interop/typescript-redis-python/package.json create mode 100755 tests/interop/typescript-redis-python/run_tests.sh create mode 100644 tests/interop/typescript-redis-python/venv/.gitignore create mode 100644 tests/interop/typescript-redis-python/venv/bin/Activate.ps1 create mode 100644 tests/interop/typescript-redis-python/venv/bin/activate create mode 100644 tests/interop/typescript-redis-python/venv/bin/activate.csh create mode 100644 tests/interop/typescript-redis-python/venv/bin/activate.fish create mode 100755 tests/interop/typescript-redis-python/venv/bin/pip create mode 100755 tests/interop/typescript-redis-python/venv/bin/pip3 create mode 100755 tests/interop/typescript-redis-python/venv/bin/pip3.11 create mode 120000 tests/interop/typescript-redis-python/venv/bin/python create mode 120000 tests/interop/typescript-redis-python/venv/bin/python3 create mode 120000 tests/interop/typescript-redis-python/venv/bin/python3.11 create mode 100644 tests/interop/typescript-redis-python/venv/pyvenv.cfg create mode 100644 tests/interop/typescript-redis-python/verify_state.py diff --git a/tests/interop/README.md b/tests/interop/README.md new file mode 100644 index 0000000..d6c7d0e --- /dev/null +++ b/tests/interop/README.md @@ -0,0 +1,94 @@ +# BrowserState Interop Tests + +This directory contains interoperability tests between Python and TypeScript implementations of BrowserState using Redis as the storage backend. + +## Directory Structure + +``` +tests/interop/ +├── python-redis-typescript/ # Python -> Redis -> TypeScript tests +├── typescript-redis-python/ # TypeScript -> Redis -> Python tests +├── setup.sh # Setup script for all interop tests +└── run_all.sh # Run all interop tests +``` + +## Prerequisites + +1. Redis server running locally (default port 6379) +2. Python 3.7+ +3. Node.js with npm +4. TypeScript and ts-node + +## Setup + +Run the setup script to prepare the test environments: + +```bash +./setup.sh +``` + +This will: +1. Create virtual environments for the test directories +2. Install the Python package in development mode +3. Install the TypeScript package in development mode +4. Set up all necessary dependencies + +## Running Tests + +To run all interop tests: + +```bash +./run_all.sh +``` + +This will: +1. Check if Redis is running +2. Run each test suite in sequence +3. Report success or failure for each test + +## Test Structure + +Each test directory contains: +- A Python script that creates/verifies browser state data +- A TypeScript script that creates/verifies browser state data +- A shell script (`run_tests.sh`) that orchestrates the test execution + +## Cross-Language Interoperability + +These tests verify that: + +1. Browser state created in Python can be correctly accessed in TypeScript +2. Browser state created in TypeScript can be correctly accessed in Python + +The tests focus on localStorage data persistence through Redis storage. + +## GitHub Workflow Integration + +These tests are designed to be run in GitHub workflows. Example workflow step: + +```yaml +- name: Run Interop Tests + run: | + cd tests/interop + ./setup.sh + ./run_all.sh +``` + +## Troubleshooting + +1. If Redis connection fails: + ```bash + redis-cli ping + ``` + +2. If Python package is not found: + ```bash + cd tests/interop + ./setup.sh + ``` + +3. If TypeScript package is not found: + ```bash + cd tests/interop + ./setup.sh + ``` \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/README.md b/tests/interop/python-redis-typescript/README.md new file mode 100644 index 0000000..c717d2c --- /dev/null +++ b/tests/interop/python-redis-typescript/README.md @@ -0,0 +1,67 @@ +# Python -> Redis -> TypeScript Interoperability Test + +This test verifies the cross-language interoperability between Python and TypeScript implementations of BrowserState using Redis as the storage backend. + +## Overview + +The test performs the following: + +1. Uses Python to create browser state data (localStorage entries) using Playwright +2. Stores the state in Redis +3. Uses TypeScript to load the state from Redis using Playwright +4. Verifies that the state created in Python can be correctly read in TypeScript + +## Requirements + +- Redis server running on localhost:6379 +- Python 3.6+ with pip +- Node.js with npm +- BrowserState Python package installed +- BrowserState TypeScript package installed + +## Files + +- `test_cross_language.py`: Python script that creates state data and saves it to Redis +- `verify_state.ts`: TypeScript script that loads state from Redis and verifies it +- `run_tests.sh`: Shell script that installs dependencies and runs both tests in sequence + +## Running the Tests + +To run the tests, execute: + +```bash +./run_tests.sh +``` + +The script will: +1. Check if Redis is running +2. Install required dependencies (Playwright for both Python and TypeScript) +3. Run the Python test to create state +4. Run the TypeScript test to verify state + +## How it Works + +### Python Side + +The Python script: +1. Initializes a BrowserState instance with Redis storage +2. Mounts a session with a specific ID +3. Uses Playwright to load a test HTML page +4. Adds notes to localStorage in the browser +5. Unmounts the session, which saves the state to Redis + +### TypeScript Side + +The TypeScript script: +1. Initializes a BrowserState instance with the same Redis configuration +2. Mounts the same session ID +3. Uses Playwright to load the same test HTML page +4. Reads the localStorage data created by Python +5. Verifies that the data created by Python exists and is correct + +## Troubleshooting + +- If Redis is not running, start it with `brew services start redis` (macOS) or `redis-server` (Linux) +- If dependencies are not installed correctly, you can install them manually: + - For Python: `pip install playwright && python -m playwright install chromium` + - For TypeScript: `npm install playwright ts-node` \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/create_state.py b/tests/interop/python-redis-typescript/create_state.py new file mode 100644 index 0000000..7e97790 --- /dev/null +++ b/tests/interop/python-redis-typescript/create_state.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +Example demonstrating Python to TypeScript interop with Redis storage. +This script creates a browser state in Python and stores it in Redis, +which can then be read by the TypeScript implementation. +""" + +import os +import json +import tempfile +import shutil +from pathlib import Path +from browserstate import BrowserState, RedisStorage + +# Redis configuration matching TypeScript example +REDIS_CONFIG = { + "host": "localhost", + "port": 6379, + "password": None, + "db": 0, + "key_prefix": "browserstate:", + "ttl": 604800 # 7 days +} + +def create_test_data(base_dir: str) -> None: + """Create test data in the specified directory.""" + # Create a test file + test_file = Path(base_dir) / "test.txt" + test_file.write_text("Test data from Python implementation") + + # Create a subdirectory with nested files + subdir = Path(base_dir) / "subdir" + subdir.mkdir(exist_ok=True) + (subdir / "nested.txt").write_text("Nested file from Python") + + # Create a JSON file with metadata + metadata = { + "created_by": "python", + "timestamp": "2024-04-01T00:00:00Z", + "version": "1.0.0" + } + (base_dir / "metadata.json").write_text(json.dumps(metadata, indent=2)) + +def main(): + print("🚀 Starting Python to TypeScript Redis Interop Demo\n") + + # Create temporary directory for test data + with tempfile.TemporaryDirectory() as temp_dir: + print(f"📂 Created temporary directory: {temp_dir}") + + # Create test data + create_test_data(Path(temp_dir)) + print("📝 Created test files") + + # Initialize browser state with Redis storage + print("\n🔧 Initializing BrowserState with Redis storage...") + browser_state = BrowserState( + user_id="interop_test_user", + storage_type="redis", + redis_options=REDIS_CONFIG + ) + + # Upload the test data + print("\n📤 Uploading test data to Redis...") + session_id = "python_created_session" + browser_state.upload(session_id, temp_dir) + + # Verify the upload + print("\n✅ Verification:") + print(f" - User ID: interop_test_user") + print(f" - Session ID: {session_id}") + print(f" - Key prefix: {REDIS_CONFIG['key_prefix']}") + + # List sessions to verify + sessions = browser_state.list_sessions() + print(f"\n📋 Available sessions: {sessions}") + + print("\n✨ Python state creation complete!") + print("You can now run the TypeScript example to verify the state") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/package-lock.json b/tests/interop/python-redis-typescript/package-lock.json new file mode 100644 index 0000000..557e82f --- /dev/null +++ b/tests/interop/python-redis-typescript/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "python-redis-typescript", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "python-redis-typescript", + "version": "1.0.0", + "license": "ISC" + } + } +} diff --git a/tests/interop/python-redis-typescript/package.json b/tests/interop/python-redis-typescript/package.json new file mode 100644 index 0000000..a8560a4 --- /dev/null +++ b/tests/interop/python-redis-typescript/package.json @@ -0,0 +1,12 @@ +{ + "name": "python-redis-typescript", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "" +} diff --git a/tests/interop/python-redis-typescript/run_tests.sh b/tests/interop/python-redis-typescript/run_tests.sh new file mode 100755 index 0000000..d1da01d --- /dev/null +++ b/tests/interop/python-redis-typescript/run_tests.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +# Exit on error +set -e + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print section headers +print_header() { + echo -e "\n${BLUE}=== $1 ===${NC}\n" +} + +# Function to check if Redis is running +check_redis() { + print_header "Checking Redis Connection" + if ! redis-cli ping > /dev/null 2>&1; then + echo -e "${RED}❌ Redis is not running. Please start Redis first.${NC}" + exit 1 + fi + echo -e "${GREEN}✅ Redis is running${NC}" +} + +# Function to check Python dependencies +check_python_deps() { + print_header "Checking Python Dependencies" + if ! pip show playwright > /dev/null 2>&1; then + echo -e "${BLUE}Installing Python Playwright...${NC}" + pip install playwright + python -m playwright install chromium + fi + echo -e "${GREEN}✅ Python dependencies installed${NC}" +} + +# Function to check TypeScript dependencies +check_ts_deps() { + print_header "Checking TypeScript Dependencies" + # Create package.json if it doesn't exist + if [ ! -f "package.json" ]; then + echo '{ + "name": "interop-tests", + "version": "1.0.0", + "description": "Interop tests for BrowserState", + "dependencies": {} +}' > package.json + fi + + # Install required packages + echo -e "${BLUE}Installing TypeScript dependencies...${NC}" + npm install --no-save playwright ts-node + echo -e "${GREEN}✅ TypeScript dependencies installed${NC}" +} + +# Main execution +echo -e "${BLUE}🚀 Starting Python -> Redis -> TypeScript Interop Test${NC}" + +# Check prerequisites +check_redis +check_python_deps +check_ts_deps + +# Run Python test +print_header "Running Python Test" +if ! python3 test_cross_language.py; then + echo -e "${RED}❌ Python test failed${NC}" + exit 1 +fi +echo -e "${GREEN}✅ Python test completed successfully${NC}" + +# Run TypeScript verification +print_header "Running TypeScript Verification" +if ! ts-node verify_state.ts; then + echo -e "${RED}❌ TypeScript verification failed${NC}" + exit 1 +fi +echo -e "${GREEN}✅ TypeScript verification completed successfully${NC}" + +echo -e "\n${GREEN}✨ All tests completed successfully!${NC}" \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/test_cross_language.py b/tests/interop/python-redis-typescript/test_cross_language.py new file mode 100644 index 0000000..52b83c6 --- /dev/null +++ b/tests/interop/python-redis-typescript/test_cross_language.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +Cross-language interop test using Playwright to verify browser state persistence +between Python and TypeScript implementations. +""" + +import os +import json +import asyncio +from pathlib import Path +from playwright.async_api import async_playwright +from browserstate import BrowserState, RedisStorage + +# Redis configuration matching TypeScript example +REDIS_CONFIG = { + "host": "localhost", + "port": 6379, + "password": None, + "db": 0, + "key_prefix": "browserstate:", + "ttl": 604800 # 7 days +} + +# Path to the test HTML file +TEST_HTML_PATH = Path(__file__).parent.parent.parent.parent / "typescript" / "examples" / "shared" / "test.html" + +async def create_test_data(browser_state: BrowserState, session_id: str) -> None: + """Create test data using Playwright and store it in Redis.""" + print(f"\n🔧 Creating test data in session: {session_id}") + + # Mount the session + state = browser_state.mount_session(session_id) + print(f"📂 Mounted session at: {state['path']}") + + async with async_playwright() as p: + # Launch browser with the mounted state + browser = await p.chromium.launch_persistent_context( + user_data_dir=state["path"], + headless=False + ) + + try: + # Create a new page + page = await browser.new_page() + + # Navigate to the test HTML page + await page.goto(f"file://{TEST_HTML_PATH}") + print("📄 Loaded test page") + + # Add some test notes + test_notes = [ + "Python created note 1", + "Python created note 2", + "Python created note 3" + ] + + for note in test_notes: + await page.fill("#noteInput", note) + await page.click("button:text('Add Note')") + await page.wait_for_timeout(500) # Wait for animation + + # Verify notes were added + notes_count = await page.evaluate("""() => { + return JSON.parse(localStorage.getItem('notes') || '[]').length; + }""") + print(f"✅ Added {notes_count} notes") + + # Get the notes data for verification + notes_data = await page.evaluate("""() => { + return localStorage.getItem('notes'); + }""") + print(f"📝 Notes data: {notes_data}") + + finally: + await browser.close() + + # Unmount the session to save changes + browser_state.unmount_session() + print("💾 Saved session state to Redis") + +async def verify_test_data(browser_state: BrowserState, session_id: str) -> None: + """Verify that the test data can be read by TypeScript implementation.""" + print(f"\n🔍 Verifying test data in session: {session_id}") + + # Mount the session + state = browser_state.mount_session(session_id) + print(f"📂 Mounted session at: {state['path']}") + + async with async_playwright() as p: + # Launch browser with the mounted state + browser = await p.chromium.launch_persistent_context( + user_data_dir=state["path"], + headless=False + ) + + try: + # Create a new page + page = await browser.new_page() + + # Navigate to the test HTML page + await page.goto(f"file://{TEST_HTML_PATH}") + print("📄 Loaded test page") + + # Get the notes data + notes_data = await page.evaluate("""() => { + return localStorage.getItem('notes'); + }""") + + if notes_data: + notes = json.loads(notes_data) + print(f"📝 Found {len(notes)} notes:") + for note in notes: + print(f" - {note['text']} ({note['timestamp']})") + else: + print("❌ No notes found in localStorage") + + finally: + await browser.close() + + # Unmount the session + browser_state.unmount_session() + print("✅ Verification complete") + +async def main(): + """Main function to run the cross-language interop test.""" + print("🚀 Starting Python -> Redis -> TypeScript Interop Test\n") + + # Initialize browser state with Redis storage + browser_state = BrowserState( + user_id="interop_test_user", + storage_type="redis", + redis_options=REDIS_CONFIG + ) + + # Test session ID + session_id = "cross_language_test" + + # Create test data with Python + await create_test_data(browser_state, session_id) + + # Verify the data can be read + await verify_test_data(browser_state, session_id) + + print("\n✨ Test completed successfully!") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/venv/.gitignore b/tests/interop/python-redis-typescript/venv/.gitignore new file mode 100644 index 0000000..61d97f0 --- /dev/null +++ b/tests/interop/python-redis-typescript/venv/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/venv/bin/Activate.ps1 b/tests/interop/python-redis-typescript/venv/bin/Activate.ps1 new file mode 100644 index 0000000..b49d77b --- /dev/null +++ b/tests/interop/python-redis-typescript/venv/bin/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/tests/interop/python-redis-typescript/venv/bin/activate b/tests/interop/python-redis-typescript/venv/bin/activate new file mode 100644 index 0000000..2deb9db --- /dev/null +++ b/tests/interop/python-redis-typescript/venv/bin/activate @@ -0,0 +1,69 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV="/Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv" +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1="(venv) ${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT="(venv) " + export VIRTUAL_ENV_PROMPT +fi + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null +fi diff --git a/tests/interop/python-redis-typescript/venv/bin/activate.csh b/tests/interop/python-redis-typescript/venv/bin/activate.csh new file mode 100644 index 0000000..7f6a931 --- /dev/null +++ b/tests/interop/python-redis-typescript/venv/bin/activate.csh @@ -0,0 +1,26 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV "/Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv" + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/bin:$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = "(venv) $prompt" + setenv VIRTUAL_ENV_PROMPT "(venv) " +endif + +alias pydoc python -m pydoc + +rehash diff --git a/tests/interop/python-redis-typescript/venv/bin/activate.fish b/tests/interop/python-redis-typescript/venv/bin/activate.fish new file mode 100644 index 0000000..e86f9aa --- /dev/null +++ b/tests/interop/python-redis-typescript/venv/bin/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/); you cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV "/Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv" + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/bin" $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT "(venv) " +end diff --git a/tests/interop/python-redis-typescript/venv/bin/pip b/tests/interop/python-redis-typescript/venv/bin/pip new file mode 100755 index 0000000..bb64f9f --- /dev/null +++ b/tests/interop/python-redis-typescript/venv/bin/pip @@ -0,0 +1,8 @@ +#!/Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv/bin/python3.11 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/tests/interop/python-redis-typescript/venv/bin/pip3 b/tests/interop/python-redis-typescript/venv/bin/pip3 new file mode 100755 index 0000000..bb64f9f --- /dev/null +++ b/tests/interop/python-redis-typescript/venv/bin/pip3 @@ -0,0 +1,8 @@ +#!/Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv/bin/python3.11 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/tests/interop/python-redis-typescript/venv/bin/pip3.11 b/tests/interop/python-redis-typescript/venv/bin/pip3.11 new file mode 100755 index 0000000..bb64f9f --- /dev/null +++ b/tests/interop/python-redis-typescript/venv/bin/pip3.11 @@ -0,0 +1,8 @@ +#!/Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv/bin/python3.11 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/tests/interop/python-redis-typescript/venv/bin/python b/tests/interop/python-redis-typescript/venv/bin/python new file mode 120000 index 0000000..6e7f3c7 --- /dev/null +++ b/tests/interop/python-redis-typescript/venv/bin/python @@ -0,0 +1 @@ +python3.11 \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/venv/bin/python3 b/tests/interop/python-redis-typescript/venv/bin/python3 new file mode 120000 index 0000000..6e7f3c7 --- /dev/null +++ b/tests/interop/python-redis-typescript/venv/bin/python3 @@ -0,0 +1 @@ +python3.11 \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/venv/bin/python3.11 b/tests/interop/python-redis-typescript/venv/bin/python3.11 new file mode 120000 index 0000000..2d17577 --- /dev/null +++ b/tests/interop/python-redis-typescript/venv/bin/python3.11 @@ -0,0 +1 @@ +/Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11 \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/venv/pyvenv.cfg b/tests/interop/python-redis-typescript/venv/pyvenv.cfg new file mode 100644 index 0000000..3ead705 --- /dev/null +++ b/tests/interop/python-redis-typescript/venv/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /Library/Frameworks/Python.framework/Versions/3.11/bin +include-system-site-packages = false +version = 3.11.6 +executable = /Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11 +command = /Users/boateng/Documents/work/browserstate/tests/interop/python-redis-python/venv/bin/python3 -m venv /Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv diff --git a/tests/interop/python-redis-typescript/verify_state.ts b/tests/interop/python-redis-typescript/verify_state.ts new file mode 100644 index 0000000..b43d57b --- /dev/null +++ b/tests/interop/python-redis-typescript/verify_state.ts @@ -0,0 +1,99 @@ +/** + * TypeScript verification script for cross-language interop test. + * This script verifies browser state created by the Python implementation. + */ + +import path from 'path'; +import fs from 'fs'; +import { BrowserState } from '../../../typescript/src/BrowserState'; +import { chromium } from 'playwright'; + +// Redis configuration matching Python example +const REDIS_CONFIG = { + host: 'localhost', + port: 6379, + password: undefined, + db: 0, + keyPrefix: 'browserstate:', + ttl: 604800 // 7 days +}; + +// Path to the test HTML file +const TEST_HTML_PATH = path.join(__dirname, '../../../typescript/examples/shared/test.html'); + +async function verifyPythonState() { + console.log('🚀 Starting TypeScript Verification of Python State\n'); + + try { + // Initialize BrowserState with Redis storage + console.log('🔧 Creating BrowserState with Redis storage...'); + const browserState = new BrowserState({ + userId: 'interop_test_user', + storageType: 'redis', + redisOptions: REDIS_CONFIG + }); + + // Test session ID + const sessionId = 'cross_language_test'; + + // Mount the session + console.log(`\n📥 Mounting session: ${sessionId}`); + const userDataDir = await browserState.mount(sessionId); + console.log(`📂 Mounted at: ${userDataDir}`); + + // Launch browser with the mounted state + console.log('\n🌐 Launching browser with Playwright...'); + const browser = await chromium.launchPersistentContext(userDataDir, { + headless: false + }); + + try { + // Create a new page + const page = await browser.newPage(); + + // Navigate to the test HTML page + console.log('\n📄 Loading test page...'); + await page.goto(`file://${TEST_HTML_PATH}`); + console.log('✅ Test page loaded'); + + // Get the notes data + console.log('\n📝 Reading notes from localStorage...'); + const notesData = await page.evaluate(() => { + return localStorage.getItem('notes'); + }); + + if (notesData) { + const notes = JSON.parse(notesData); + console.log(`\nFound ${notes.length} notes:`); + notes.forEach((note: { text: string; timestamp: string }) => { + console.log(` - ${note.text} (${note.timestamp})`); + }); + + // Verify the notes were created by Python + const pythonNotes = notes.filter((note: { text: string }) => + note.text.startsWith('Python created note') + ); + console.log(`\n✅ Found ${pythonNotes.length} Python-created notes`); + } else { + console.log('\n❌ No notes found in localStorage'); + } + + } finally { + await browser.close(); + } + + // Unmount the session + console.log('\n🔒 Unmounting session...'); + await browserState.unmount(); + console.log('✅ Session unmounted'); + + console.log('\n✨ Verification complete!'); + + } catch (error) { + console.error('\n❌ Error during verification:', error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} + +// Run the verification +verifyPythonState(); \ No newline at end of file diff --git a/tests/interop/run_all.sh b/tests/interop/run_all.sh new file mode 100755 index 0000000..4bd031f --- /dev/null +++ b/tests/interop/run_all.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# Exit on error +set -e + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print section headers +print_header() { + echo -e "\n${BLUE}=== $1 ===${NC}\n" +} + +# Function to check if Redis is running +check_redis() { + print_header "Checking Redis Connection" + if ! redis-cli ping > /dev/null 2>&1; then + echo -e "${RED}❌ Redis is not running. Please start Redis first.${NC}" + exit 1 + fi + echo -e "${GREEN}✅ Redis is running${NC}" +} + +# Function to run a test suite +run_test_suite() { + local dir=$1 + local name=$2 + print_header "Running $name Tests" + + cd "$dir" + + # Activate Python virtual environment + source venv/bin/activate + + # Run the test script + if [ -f "run_tests.sh" ]; then + if ! ./run_tests.sh; then + echo -e "${RED}❌ $name tests failed${NC}" + exit 1 + fi + else + echo -e "${RED}❌ No test script found in $dir${NC}" + exit 1 + fi + + cd .. + echo -e "${GREEN}✅ $name tests completed successfully${NC}" +} + +# Main execution +echo -e "${BLUE}🚀 Starting BrowserState Interop Tests${NC}" + +# Check Redis +check_redis + +# Run all test suites +run_test_suite "python-redis-typescript" "Python -> Redis -> TypeScript" +run_test_suite "typescript-redis-python" "TypeScript -> Redis -> Python" + +echo -e "\n${GREEN}✨ All interop tests completed successfully!${NC}" \ No newline at end of file diff --git a/tests/interop/setup.sh b/tests/interop/setup.sh new file mode 100755 index 0000000..5c76aa2 --- /dev/null +++ b/tests/interop/setup.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Exit on error +set -e + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print section headers +print_header() { + echo -e "\n${BLUE}=== $1 ===${NC}\n" +} + +# Function to setup a test directory +setup_test_dir() { + local dir=$1 + local name=$2 + print_header "Setting up $name test environment" + + # Create directory if it doesn't exist + mkdir -p "$dir" + cd "$dir" + + # Create and activate Python virtual environment + python3 -m venv venv + source venv/bin/activate + + # Install Python package + pip install -e ../../../python + + # Initialize npm and install TypeScript package + npm init -y + npm install -e ../../../typescript + + # Install ts-node globally + npm install -g ts-node + + cd .. +} + +# Main setup +print_header "Setting up BrowserState Interop Test Environment" + +# Create test directories +mkdir -p python-redis-typescript +mkdir -p typescript-redis-python + +# Setup each test directory +setup_test_dir "python-redis-typescript" "Python -> Redis -> TypeScript" +setup_test_dir "typescript-redis-python" "TypeScript -> Redis -> Python" + +print_header "Setup Complete" +echo -e "${GREEN}✅ All test environments are ready${NC}" +echo -e "\nYou can now run the tests using:" +echo -e " ./run_all.sh" \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/README.md b/tests/interop/typescript-redis-python/README.md new file mode 100644 index 0000000..9d4c6bb --- /dev/null +++ b/tests/interop/typescript-redis-python/README.md @@ -0,0 +1,67 @@ +# TypeScript -> Redis -> Python Interoperability Test + +This test verifies the cross-language interoperability between TypeScript and Python implementations of BrowserState using Redis as the storage backend. + +## Overview + +The test performs the following: + +1. Uses TypeScript to create browser state data (localStorage entries) using Playwright +2. Stores the state in Redis +3. Uses Python to load the state from Redis using Playwright +4. Verifies that the state created in TypeScript can be correctly read in Python + +## Requirements + +- Redis server running on localhost:6379 +- Python 3.6+ with pip +- Node.js with npm +- BrowserState Python package installed +- BrowserState TypeScript package installed + +## Files + +- `create_state.ts`: TypeScript script that creates state data and saves it to Redis +- `verify_state.py`: Python script that loads state from Redis and verifies it +- `run_tests.sh`: Shell script that installs dependencies and runs both tests in sequence + +## Running the Tests + +To run the tests, execute: + +```bash +./run_tests.sh +``` + +The script will: +1. Check if Redis is running +2. Install required dependencies (Playwright for both Python and TypeScript) +3. Run the TypeScript test to create state +4. Run the Python test to verify state + +## How it Works + +### TypeScript Side + +The TypeScript script: +1. Initializes a BrowserState instance with Redis storage +2. Mounts a session with a specific ID +3. Uses Playwright to load a test HTML page +4. Adds notes to localStorage in the browser +5. Unmounts the session, which saves the state to Redis + +### Python Side + +The Python script: +1. Initializes a BrowserState instance with the same Redis configuration +2. Mounts the same session ID +3. Uses Playwright to load the same test HTML page +4. Reads the localStorage data created by TypeScript +5. Verifies that the data created by TypeScript exists and is correct + +## Troubleshooting + +- If Redis is not running, start it with `brew services start redis` (macOS) or `redis-server` (Linux) +- If dependencies are not installed correctly, you can install them manually: + - For Python: `pip install playwright && python -m playwright install chromium` + - For TypeScript: `npm install playwright ts-node` \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/create_state.ts b/tests/interop/typescript-redis-python/create_state.ts new file mode 100644 index 0000000..af95865 --- /dev/null +++ b/tests/interop/typescript-redis-python/create_state.ts @@ -0,0 +1,107 @@ +/** + * TypeScript script to create browser state data using localStorage + * for cross-language interoperability testing. + */ + +import path from 'path'; +import { BrowserState } from '../../../typescript/src/BrowserState'; +import { chromium } from 'playwright'; + +// Redis configuration +const REDIS_CONFIG = { + host: 'localhost', + port: 6379, + password: undefined, + db: 0, + keyPrefix: 'browserstate:', + ttl: 604800 // 7 days +}; + +// Path to the test HTML file +const TEST_HTML_PATH = path.join(__dirname, '../../../typescript/examples/shared/test.html'); + +async function createTestData() { + console.log('🚀 Starting TypeScript State Creation\n'); + + try { + // Initialize BrowserState with Redis storage + console.log('🔧 Creating BrowserState with Redis storage...'); + const browserState = new BrowserState({ + userId: 'interop_test_user', + storageType: 'redis', + redisOptions: REDIS_CONFIG + }); + + // Test session ID + const sessionId = 'typescript_to_python_test'; + + // List existing sessions + console.log('\n📋 Listing existing sessions...'); + const sessions = await browserState.listSessions(); + console.log(`Found ${sessions.length} session(s): ${sessions.join(', ')}`); + + // Mount the session + console.log(`\n📥 Mounting session: ${sessionId}`); + const userDataDir = await browserState.mount(sessionId); + console.log(`📂 Mounted at: ${userDataDir}`); + + // Launch browser with the mounted state + console.log('\n🌐 Launching browser with Playwright...'); + const browser = await chromium.launchPersistentContext(userDataDir, { + headless: false + }); + + try { + // Create a new page + const page = await browser.newPage(); + + // Navigate to the test HTML page + console.log('\n📄 Loading test page...'); + await page.goto(`file://${TEST_HTML_PATH}`); + console.log('✅ Test page loaded'); + + // Add notes to localStorage + console.log('\n📝 Adding notes to localStorage...'); + const testNotes = [ + 'TypeScript created note 1', + 'TypeScript created note 2', + 'TypeScript created note 3' + ]; + + for (const note of testNotes) { + await page.fill('#noteInput', note); + await page.click('button:text("Add Note")'); + await page.waitForTimeout(500); // Wait for animation + } + + // Verify notes were added + const notesCount = await page.evaluate(() => { + return JSON.parse(localStorage.getItem('notes') || '[]').length; + }); + console.log(`✅ Added ${notesCount} notes`); + + // Get the notes data for verification + const notesData = await page.evaluate(() => { + return localStorage.getItem('notes'); + }); + console.log(`📝 Notes data: ${notesData}`); + + } finally { + await browser.close(); + } + + // Unmount the session + console.log('\n🔒 Unmounting session...'); + await browserState.unmount(); + console.log('✅ Session unmounted'); + + console.log('\n✨ State creation complete!'); + + } catch (error) { + console.error('\n❌ Error during state creation:', error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} + +// Run the state creation +createTestData(); \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/package-lock.json b/tests/interop/typescript-redis-python/package-lock.json new file mode 100644 index 0000000..ff7af30 --- /dev/null +++ b/tests/interop/typescript-redis-python/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "typescript-redis-python", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "typescript-redis-python", + "version": "1.0.0", + "license": "ISC" + } + } +} diff --git a/tests/interop/typescript-redis-python/package.json b/tests/interop/typescript-redis-python/package.json new file mode 100644 index 0000000..4337cb8 --- /dev/null +++ b/tests/interop/typescript-redis-python/package.json @@ -0,0 +1,12 @@ +{ + "name": "typescript-redis-python", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "" +} diff --git a/tests/interop/typescript-redis-python/run_tests.sh b/tests/interop/typescript-redis-python/run_tests.sh new file mode 100755 index 0000000..68d9cdb --- /dev/null +++ b/tests/interop/typescript-redis-python/run_tests.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +# Exit on error +set -e + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print section headers +print_header() { + echo -e "\n${BLUE}=== $1 ===${NC}\n" +} + +# Function to check if Redis is running +check_redis() { + print_header "Checking Redis Connection" + if ! redis-cli ping > /dev/null 2>&1; then + echo -e "${RED}❌ Redis is not running. Please start Redis first.${NC}" + exit 1 + fi + echo -e "${GREEN}✅ Redis is running${NC}" +} + +# Function to check Python dependencies +check_python_deps() { + print_header "Checking Python Dependencies" + if ! pip show playwright > /dev/null 2>&1; then + echo -e "${BLUE}Installing Python Playwright...${NC}" + pip install playwright + python -m playwright install chromium + fi + echo -e "${GREEN}✅ Python dependencies installed${NC}" +} + +# Function to check TypeScript dependencies +check_ts_deps() { + print_header "Checking TypeScript Dependencies" + # Create package.json if it doesn't exist + if [ ! -f "package.json" ]; then + echo '{ + "name": "interop-tests", + "version": "1.0.0", + "description": "Interop tests for BrowserState", + "dependencies": {} +}' > package.json + fi + + # Install required packages + echo -e "${BLUE}Installing TypeScript dependencies...${NC}" + npm install --no-save playwright ts-node + echo -e "${GREEN}✅ TypeScript dependencies installed${NC}" +} + +# Main execution +echo -e "${BLUE}🚀 Starting TypeScript -> Redis -> Python Interop Test${NC}" + +# Check prerequisites +check_redis +check_python_deps +check_ts_deps + +# Run TypeScript state creation +print_header "Running TypeScript State Creation" +if ! ts-node create_state.ts; then + echo -e "${RED}❌ TypeScript state creation failed${NC}" + exit 1 +fi +echo -e "${GREEN}✅ TypeScript state creation completed successfully${NC}" + +# Run Python verification +print_header "Running Python Verification" +if ! python3 verify_state.py; then + echo -e "${RED}❌ Python verification failed${NC}" + exit 1 +fi +echo -e "${GREEN}✅ Python verification completed successfully${NC}" + +echo -e "\n${GREEN}✨ All tests completed successfully!${NC}" \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/venv/.gitignore b/tests/interop/typescript-redis-python/venv/.gitignore new file mode 100644 index 0000000..61d97f0 --- /dev/null +++ b/tests/interop/typescript-redis-python/venv/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/venv/bin/Activate.ps1 b/tests/interop/typescript-redis-python/venv/bin/Activate.ps1 new file mode 100644 index 0000000..b49d77b --- /dev/null +++ b/tests/interop/typescript-redis-python/venv/bin/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/tests/interop/typescript-redis-python/venv/bin/activate b/tests/interop/typescript-redis-python/venv/bin/activate new file mode 100644 index 0000000..d2de552 --- /dev/null +++ b/tests/interop/typescript-redis-python/venv/bin/activate @@ -0,0 +1,69 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV="/Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv" +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1="(venv) ${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT="(venv) " + export VIRTUAL_ENV_PROMPT +fi + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null +fi diff --git a/tests/interop/typescript-redis-python/venv/bin/activate.csh b/tests/interop/typescript-redis-python/venv/bin/activate.csh new file mode 100644 index 0000000..1d98448 --- /dev/null +++ b/tests/interop/typescript-redis-python/venv/bin/activate.csh @@ -0,0 +1,26 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV "/Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv" + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/bin:$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = "(venv) $prompt" + setenv VIRTUAL_ENV_PROMPT "(venv) " +endif + +alias pydoc python -m pydoc + +rehash diff --git a/tests/interop/typescript-redis-python/venv/bin/activate.fish b/tests/interop/typescript-redis-python/venv/bin/activate.fish new file mode 100644 index 0000000..a9f566a --- /dev/null +++ b/tests/interop/typescript-redis-python/venv/bin/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/); you cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV "/Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv" + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/bin" $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT "(venv) " +end diff --git a/tests/interop/typescript-redis-python/venv/bin/pip b/tests/interop/typescript-redis-python/venv/bin/pip new file mode 100755 index 0000000..10ff886 --- /dev/null +++ b/tests/interop/typescript-redis-python/venv/bin/pip @@ -0,0 +1,8 @@ +#!/Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv/bin/python3.11 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/tests/interop/typescript-redis-python/venv/bin/pip3 b/tests/interop/typescript-redis-python/venv/bin/pip3 new file mode 100755 index 0000000..10ff886 --- /dev/null +++ b/tests/interop/typescript-redis-python/venv/bin/pip3 @@ -0,0 +1,8 @@ +#!/Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv/bin/python3.11 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/tests/interop/typescript-redis-python/venv/bin/pip3.11 b/tests/interop/typescript-redis-python/venv/bin/pip3.11 new file mode 100755 index 0000000..10ff886 --- /dev/null +++ b/tests/interop/typescript-redis-python/venv/bin/pip3.11 @@ -0,0 +1,8 @@ +#!/Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv/bin/python3.11 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/tests/interop/typescript-redis-python/venv/bin/python b/tests/interop/typescript-redis-python/venv/bin/python new file mode 120000 index 0000000..6e7f3c7 --- /dev/null +++ b/tests/interop/typescript-redis-python/venv/bin/python @@ -0,0 +1 @@ +python3.11 \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/venv/bin/python3 b/tests/interop/typescript-redis-python/venv/bin/python3 new file mode 120000 index 0000000..6e7f3c7 --- /dev/null +++ b/tests/interop/typescript-redis-python/venv/bin/python3 @@ -0,0 +1 @@ +python3.11 \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/venv/bin/python3.11 b/tests/interop/typescript-redis-python/venv/bin/python3.11 new file mode 120000 index 0000000..2d17577 --- /dev/null +++ b/tests/interop/typescript-redis-python/venv/bin/python3.11 @@ -0,0 +1 @@ +/Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11 \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/venv/pyvenv.cfg b/tests/interop/typescript-redis-python/venv/pyvenv.cfg new file mode 100644 index 0000000..7f80dd2 --- /dev/null +++ b/tests/interop/typescript-redis-python/venv/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /Library/Frameworks/Python.framework/Versions/3.11/bin +include-system-site-packages = false +version = 3.11.6 +executable = /Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11 +command = /Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv/bin/python3 -m venv /Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv diff --git a/tests/interop/typescript-redis-python/verify_state.py b/tests/interop/typescript-redis-python/verify_state.py new file mode 100644 index 0000000..b4534dd --- /dev/null +++ b/tests/interop/typescript-redis-python/verify_state.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Python verification script for cross-language interop test. +This script verifies browser state created by the TypeScript implementation. +""" + +import os +import json +import asyncio +from pathlib import Path +from playwright.async_api import async_playwright +from browserstate import BrowserState, RedisStorage + +# Redis configuration matching TypeScript example +REDIS_CONFIG = { + "host": "localhost", + "port": 6379, + "password": None, + "db": 0, + "key_prefix": "browserstate:", + "ttl": 604800 # 7 days +} + +# Path to the test HTML file +TEST_HTML_PATH = Path(__file__).parent.parent.parent.parent / "typescript" / "examples" / "shared" / "test.html" + +async def verify_typescript_state(): + """Verify the browser state created by TypeScript.""" + print("\n🔍 Verifying TypeScript-created browser state") + + # Initialize browser state with Redis storage + browser_state = BrowserState( + user_id="interop_test_user", + storage_type="redis", + redis_options=REDIS_CONFIG + ) + + # Test session ID + session_id = "typescript_to_python_test" + + # List available sessions + sessions = browser_state.list_sessions() + print(f"📋 Available sessions: {sessions}") + + if session_id not in sessions: + print(f"❌ Session '{session_id}' not found") + return + + # Mount the session + state = browser_state.mount_session(session_id) + print(f"📂 Mounted session at: {state['path']}") + + async with async_playwright() as p: + # Launch browser with the mounted state + browser = await p.chromium.launch_persistent_context( + user_data_dir=state["path"], + headless=False + ) + + try: + # Create a new page + page = await browser.new_page() + + # Navigate to the test HTML page + await page.goto(f"file://{TEST_HTML_PATH}") + print("📄 Loaded test page") + + # Get the notes data + notes_data = await page.evaluate("""() => { + return localStorage.getItem('notes'); + }""") + + if notes_data: + notes = json.loads(notes_data) + print(f"📝 Found {len(notes)} notes:") + for note in notes: + print(f" - {note['text']} ({note['timestamp']})") + + # Verify the notes were created by TypeScript + typescript_notes = [note for note in notes if note['text'].startswith('TypeScript created note')] + if typescript_notes: + print(f"✅ Found {len(typescript_notes)} TypeScript-created notes") + else: + print("❌ No TypeScript-created notes found") + else: + print("❌ No notes found in localStorage") + + finally: + await browser.close() + + # Unmount the session + browser_state.unmount_session() + print("✅ Verification complete") + +if __name__ == "__main__": + asyncio.run(verify_typescript_state()) \ No newline at end of file From ee69d3dee7af7a5bb18eb2f71d86b03e102bab4e Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 03:47:54 +0100 Subject: [PATCH 02/20] Update interop tests to work with proper Python and TypeScript ES module support --- tests/interop/README.md | 72 ++++- .../python-redis-typescript/package-lock.json | 266 ++++++++++++++++++ .../python-redis-typescript/package.json | 19 +- .../python-redis-typescript/run_tests.sh | 50 ++-- .../test_cross_language.py | 24 +- .../python-redis-typescript/venv/pyvenv.cfg | 2 +- .../python-redis-typescript/verify_state.ts | 11 +- tests/interop/run_all.sh | 5 +- tests/interop/setup.sh | 12 +- .../typescript-redis-python/create_state.ts | 7 +- .../typescript-redis-python/package.json | 3 +- .../typescript-redis-python/run_tests.sh | 50 ++-- .../typescript-redis-python/verify_state.py | 24 +- 13 files changed, 447 insertions(+), 98 deletions(-) diff --git a/tests/interop/README.md b/tests/interop/README.md index d6c7d0e..b98c6d4 100644 --- a/tests/interop/README.md +++ b/tests/interop/README.md @@ -29,9 +29,11 @@ Run the setup script to prepare the test environments: This will: 1. Create virtual environments for the test directories -2. Install the Python package in development mode -3. Install the TypeScript package in development mode -4. Set up all necessary dependencies +2. Install all required dependencies: + - Python: boto3, google-cloud-storage, redis, playwright + - TypeScript: playwright, ts-node +3. Install the Python and TypeScript packages in development mode +4. Install and configure the Playwright browser automation tool ## Running Tests @@ -43,8 +45,9 @@ To run all interop tests: This will: 1. Check if Redis is running -2. Run each test suite in sequence -3. Report success or failure for each test +2. Verify that all required dependencies are installed +3. Run each test suite in sequence +4. Report success or failure for each test ## Test Structure @@ -67,11 +70,51 @@ The tests focus on localStorage data persistence through Redis storage. These tests are designed to be run in GitHub workflows. Example workflow step: ```yaml -- name: Run Interop Tests - run: | - cd tests/interop - ./setup.sh - ./run_all.sh +name: Interop Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + interop-tests: + runs-on: ubuntu-latest + + services: + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Setup Interop Test Environment + run: | + cd tests/interop + ./setup.sh + + - name: Run Interop Tests + run: | + cd tests/interop + ./run_all.sh ``` ## Troubleshooting @@ -81,14 +124,15 @@ These tests are designed to be run in GitHub workflows. Example workflow step: redis-cli ping ``` -2. If Python package is not found: +2. If dependencies are missing: ```bash cd tests/interop ./setup.sh ``` -3. If TypeScript package is not found: +3. If Playwright browser installation fails: ```bash - cd tests/interop - ./setup.sh + cd tests/interop/python-redis-typescript + source venv/bin/activate + python -m playwright install --help ``` \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/package-lock.json b/tests/interop/python-redis-typescript/package-lock.json index 557e82f..d06f684 100644 --- a/tests/interop/python-redis-typescript/package-lock.json +++ b/tests/interop/python-redis-typescript/package-lock.json @@ -7,7 +7,273 @@ "": { "name": "python-redis-typescript", "version": "1.0.0", + "license": "ISC", + "dependencies": { + "acorn": "^8.14.1", + "acorn-walk": "^8.3.4", + "arg": "^4.1.3", + "create-require": "^1.1.1", + "diff": "^4.0.2", + "make-error": "^1.3.6", + "playwright": "^1.51.1", + "playwright-core": "^1.51.1", + "ts-node": "^10.9.2", + "typescript": "^5.8.2", + "undici-types": "^6.20.0", + "v8-compile-cache-lib": "^3.0.1", + "yn": "^3.1.1" + }, + "devDependencies": {} + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", + "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT", + "peer": true + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "license": "ISC" + }, + "node_modules/playwright": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", + "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", + "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } } } } diff --git a/tests/interop/python-redis-typescript/package.json b/tests/interop/python-redis-typescript/package.json index a8560a4..d6bfbbf 100644 --- a/tests/interop/python-redis-typescript/package.json +++ b/tests/interop/python-redis-typescript/package.json @@ -2,11 +2,28 @@ "name": "python-redis-typescript", "version": "1.0.0", "main": "index.js", + "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", - "description": "" + "description": "This test verifies the cross-language interoperability between Python and TypeScript implementations of BrowserState using Redis as the storage backend.", + "dependencies": { + "acorn": "^8.14.1", + "acorn-walk": "^8.3.4", + "arg": "^4.1.3", + "create-require": "^1.1.1", + "diff": "^4.0.2", + "make-error": "^1.3.6", + "playwright": "^1.51.1", + "playwright-core": "^1.51.1", + "ts-node": "^10.9.2", + "typescript": "^5.8.2", + "undici-types": "^6.20.0", + "v8-compile-cache-lib": "^3.0.1", + "yn": "^3.1.1" + }, + "devDependencies": {} } diff --git a/tests/interop/python-redis-typescript/run_tests.sh b/tests/interop/python-redis-typescript/run_tests.sh index d1da01d..817f2d2 100755 --- a/tests/interop/python-redis-typescript/run_tests.sh +++ b/tests/interop/python-redis-typescript/run_tests.sh @@ -24,34 +24,27 @@ check_redis() { echo -e "${GREEN}✅ Redis is running${NC}" } -# Function to check Python dependencies -check_python_deps() { - print_header "Checking Python Dependencies" - if ! pip show playwright > /dev/null 2>&1; then - echo -e "${BLUE}Installing Python Playwright...${NC}" - pip install playwright - python -m playwright install chromium - fi - echo -e "${GREEN}✅ Python dependencies installed${NC}" -} - -# Function to check TypeScript dependencies -check_ts_deps() { - print_header "Checking TypeScript Dependencies" - # Create package.json if it doesn't exist - if [ ! -f "package.json" ]; then - echo '{ - "name": "interop-tests", - "version": "1.0.0", - "description": "Interop tests for BrowserState", - "dependencies": {} -}' > package.json +# Function to check required dependencies +check_dependencies() { + print_header "Checking Required Dependencies" + + # Check Python dependencies + for pkg in boto3 redis google.cloud playwright browserstate; do + if ! python -c "import $pkg" &> /dev/null; then + echo -e "${RED}❌ Python package '$pkg' is not installed.${NC}" + echo -e "${RED}Please run the setup script first: cd .. && ./setup.sh${NC}" + exit 1 + fi + done + + # Check TypeScript dependencies using package.json + if [ ! -f "node_modules/playwright/package.json" ] || [ ! -f "node_modules/ts-node/package.json" ]; then + echo -e "${RED}❌ TypeScript dependencies are not installed.${NC}" + echo -e "${RED}Please run the setup script first: cd .. && ./setup.sh${NC}" + exit 1 fi - # Install required packages - echo -e "${BLUE}Installing TypeScript dependencies...${NC}" - npm install --no-save playwright ts-node - echo -e "${GREEN}✅ TypeScript dependencies installed${NC}" + echo -e "${GREEN}✅ All dependencies installed${NC}" } # Main execution @@ -59,8 +52,7 @@ echo -e "${BLUE}🚀 Starting Python -> Redis -> TypeScript Interop Test${NC}" # Check prerequisites check_redis -check_python_deps -check_ts_deps +check_dependencies # Run Python test print_header "Running Python Test" @@ -72,7 +64,7 @@ echo -e "${GREEN}✅ Python test completed successfully${NC}" # Run TypeScript verification print_header "Running TypeScript Verification" -if ! ts-node verify_state.ts; then +if ! node --loader ts-node/esm verify_state.ts; then echo -e "${RED}❌ TypeScript verification failed${NC}" exit 1 fi diff --git a/tests/interop/python-redis-typescript/test_cross_language.py b/tests/interop/python-redis-typescript/test_cross_language.py index 52b83c6..c70536c 100644 --- a/tests/interop/python-redis-typescript/test_cross_language.py +++ b/tests/interop/python-redis-typescript/test_cross_language.py @@ -9,15 +9,19 @@ import asyncio from pathlib import Path from playwright.async_api import async_playwright -from browserstate import BrowserState, RedisStorage +from browserstate import BrowserState, BrowserStateOptions, RedisStorage -# Redis configuration matching TypeScript example -REDIS_CONFIG = { +# Redis configuration for Python +REDIS_URL = "redis://localhost:6379/0" +REDIS_KEY_PREFIX = "browserstate:" + +# Redis configuration matching TypeScript format - for reference only +REDIS_CONFIG_TS = { "host": "localhost", "port": 6379, "password": None, "db": 0, - "key_prefix": "browserstate:", + "keyPrefix": "browserstate:", "ttl": 604800 # 7 days } @@ -125,12 +129,18 @@ async def main(): """Main function to run the cross-language interop test.""" print("🚀 Starting Python -> Redis -> TypeScript Interop Test\n") + # Create a Redis storage provider with the correct URL format + redis_options = { + "redis_url": REDIS_URL, + "key_prefix": REDIS_KEY_PREFIX + } + # Initialize browser state with Redis storage - browser_state = BrowserState( + options = BrowserStateOptions( user_id="interop_test_user", - storage_type="redis", - redis_options=REDIS_CONFIG + redis_options=redis_options ) + browser_state = BrowserState(options) # Test session ID session_id = "cross_language_test" diff --git a/tests/interop/python-redis-typescript/venv/pyvenv.cfg b/tests/interop/python-redis-typescript/venv/pyvenv.cfg index 3ead705..e5a0403 100644 --- a/tests/interop/python-redis-typescript/venv/pyvenv.cfg +++ b/tests/interop/python-redis-typescript/venv/pyvenv.cfg @@ -2,4 +2,4 @@ home = /Library/Frameworks/Python.framework/Versions/3.11/bin include-system-site-packages = false version = 3.11.6 executable = /Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11 -command = /Users/boateng/Documents/work/browserstate/tests/interop/python-redis-python/venv/bin/python3 -m venv /Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv +command = /usr/local/bin/python3 -m venv /Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv diff --git a/tests/interop/python-redis-typescript/verify_state.ts b/tests/interop/python-redis-typescript/verify_state.ts index b43d57b..86b3a1b 100644 --- a/tests/interop/python-redis-typescript/verify_state.ts +++ b/tests/interop/python-redis-typescript/verify_state.ts @@ -4,10 +4,15 @@ */ import path from 'path'; +import { fileURLToPath } from 'url'; import fs from 'fs'; -import { BrowserState } from '../../../typescript/src/BrowserState'; +import { BrowserState } from '../../../typescript/src/BrowserState.js'; import { chromium } from 'playwright'; +// Get current file directory with ES modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + // Redis configuration matching Python example const REDIS_CONFIG = { host: 'localhost', @@ -65,12 +70,12 @@ async function verifyPythonState() { if (notesData) { const notes = JSON.parse(notesData); console.log(`\nFound ${notes.length} notes:`); - notes.forEach((note: { text: string; timestamp: string }) => { + notes.forEach((note) => { console.log(` - ${note.text} (${note.timestamp})`); }); // Verify the notes were created by Python - const pythonNotes = notes.filter((note: { text: string }) => + const pythonNotes = notes.filter((note) => note.text.startsWith('Python created note') ); console.log(`\n✅ Found ${pythonNotes.length} Python-created notes`); diff --git a/tests/interop/run_all.sh b/tests/interop/run_all.sh index 4bd031f..470e23d 100755 --- a/tests/interop/run_all.sh +++ b/tests/interop/run_all.sh @@ -32,17 +32,16 @@ run_test_suite() { cd "$dir" - # Activate Python virtual environment - source venv/bin/activate - # Run the test script if [ -f "run_tests.sh" ]; then if ! ./run_tests.sh; then echo -e "${RED}❌ $name tests failed${NC}" + cd .. exit 1 fi else echo -e "${RED}❌ No test script found in $dir${NC}" + cd .. exit 1 fi diff --git a/tests/interop/setup.sh b/tests/interop/setup.sh index 5c76aa2..5af8e0f 100755 --- a/tests/interop/setup.sh +++ b/tests/interop/setup.sh @@ -28,6 +28,14 @@ setup_test_dir() { python3 -m venv venv source venv/bin/activate + # Upgrade pip + pip install --upgrade pip + + # Install Python dependencies + print_header "Installing Python dependencies" + pip install boto3 google-cloud-storage redis playwright + python -m playwright install chromium + # Install Python package pip install -e ../../../python @@ -35,8 +43,8 @@ setup_test_dir() { npm init -y npm install -e ../../../typescript - # Install ts-node globally - npm install -g ts-node + # Install ts-node and other TypeScript dependencies + npm install --no-save playwright ts-node cd .. } diff --git a/tests/interop/typescript-redis-python/create_state.ts b/tests/interop/typescript-redis-python/create_state.ts index af95865..d7808f3 100644 --- a/tests/interop/typescript-redis-python/create_state.ts +++ b/tests/interop/typescript-redis-python/create_state.ts @@ -4,9 +4,14 @@ */ import path from 'path'; -import { BrowserState } from '../../../typescript/src/BrowserState'; +import { fileURLToPath } from 'url'; +import { BrowserState } from '../../../typescript/src/BrowserState.js'; import { chromium } from 'playwright'; +// Get current file directory with ES modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + // Redis configuration const REDIS_CONFIG = { host: 'localhost', diff --git a/tests/interop/typescript-redis-python/package.json b/tests/interop/typescript-redis-python/package.json index 4337cb8..cc3970a 100644 --- a/tests/interop/typescript-redis-python/package.json +++ b/tests/interop/typescript-redis-python/package.json @@ -2,11 +2,12 @@ "name": "typescript-redis-python", "version": "1.0.0", "main": "index.js", + "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", - "description": "" + "description": "This test verifies the cross-language interoperability between TypeScript and Python implementations of BrowserState using Redis as the storage backend." } diff --git a/tests/interop/typescript-redis-python/run_tests.sh b/tests/interop/typescript-redis-python/run_tests.sh index 68d9cdb..52b5dc7 100755 --- a/tests/interop/typescript-redis-python/run_tests.sh +++ b/tests/interop/typescript-redis-python/run_tests.sh @@ -24,34 +24,27 @@ check_redis() { echo -e "${GREEN}✅ Redis is running${NC}" } -# Function to check Python dependencies -check_python_deps() { - print_header "Checking Python Dependencies" - if ! pip show playwright > /dev/null 2>&1; then - echo -e "${BLUE}Installing Python Playwright...${NC}" - pip install playwright - python -m playwright install chromium - fi - echo -e "${GREEN}✅ Python dependencies installed${NC}" -} - -# Function to check TypeScript dependencies -check_ts_deps() { - print_header "Checking TypeScript Dependencies" - # Create package.json if it doesn't exist - if [ ! -f "package.json" ]; then - echo '{ - "name": "interop-tests", - "version": "1.0.0", - "description": "Interop tests for BrowserState", - "dependencies": {} -}' > package.json +# Function to check required dependencies +check_dependencies() { + print_header "Checking Required Dependencies" + + # Check Python dependencies + for pkg in boto3 redis google.cloud playwright browserstate; do + if ! python -c "import $pkg" &> /dev/null; then + echo -e "${RED}❌ Python package '$pkg' is not installed.${NC}" + echo -e "${RED}Please run the setup script first: cd .. && ./setup.sh${NC}" + exit 1 + fi + done + + # Check TypeScript dependencies using package.json + if [ ! -f "node_modules/playwright/package.json" ] || [ ! -f "node_modules/ts-node/package.json" ]; then + echo -e "${RED}❌ TypeScript dependencies are not installed.${NC}" + echo -e "${RED}Please run the setup script first: cd .. && ./setup.sh${NC}" + exit 1 fi - # Install required packages - echo -e "${BLUE}Installing TypeScript dependencies...${NC}" - npm install --no-save playwright ts-node - echo -e "${GREEN}✅ TypeScript dependencies installed${NC}" + echo -e "${GREEN}✅ All dependencies installed${NC}" } # Main execution @@ -59,12 +52,11 @@ echo -e "${BLUE}🚀 Starting TypeScript -> Redis -> Python Interop Test${NC}" # Check prerequisites check_redis -check_python_deps -check_ts_deps +check_dependencies # Run TypeScript state creation print_header "Running TypeScript State Creation" -if ! ts-node create_state.ts; then +if ! node --loader ts-node/esm create_state.ts; then echo -e "${RED}❌ TypeScript state creation failed${NC}" exit 1 fi diff --git a/tests/interop/typescript-redis-python/verify_state.py b/tests/interop/typescript-redis-python/verify_state.py index b4534dd..d5b3cd7 100644 --- a/tests/interop/typescript-redis-python/verify_state.py +++ b/tests/interop/typescript-redis-python/verify_state.py @@ -9,15 +9,19 @@ import asyncio from pathlib import Path from playwright.async_api import async_playwright -from browserstate import BrowserState, RedisStorage +from browserstate import BrowserState, BrowserStateOptions, RedisStorage -# Redis configuration matching TypeScript example -REDIS_CONFIG = { +# Redis configuration for Python +REDIS_URL = "redis://localhost:6379/0" +REDIS_KEY_PREFIX = "browserstate:" + +# Redis configuration matching TypeScript format - for reference only +REDIS_CONFIG_TS = { "host": "localhost", "port": 6379, "password": None, "db": 0, - "key_prefix": "browserstate:", + "keyPrefix": "browserstate:", "ttl": 604800 # 7 days } @@ -28,12 +32,18 @@ async def verify_typescript_state(): """Verify the browser state created by TypeScript.""" print("\n🔍 Verifying TypeScript-created browser state") + # Create a Redis storage provider with the correct URL format + redis_options = { + "redis_url": REDIS_URL, + "key_prefix": REDIS_KEY_PREFIX + } + # Initialize browser state with Redis storage - browser_state = BrowserState( + options = BrowserStateOptions( user_id="interop_test_user", - storage_type="redis", - redis_options=REDIS_CONFIG + redis_options=redis_options ) + browser_state = BrowserState(options) # Test session ID session_id = "typescript_to_python_test" From 0411d088951aeb957b95ee9fc2c090fe57ead4f4 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 03:48:37 +0100 Subject: [PATCH 03/20] Update run_tests.sh scripts to activate virtual environments --- tests/interop/python-redis-typescript/run_tests.sh | 10 ++++++++++ tests/interop/typescript-redis-python/run_tests.sh | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/interop/python-redis-typescript/run_tests.sh b/tests/interop/python-redis-typescript/run_tests.sh index 817f2d2..b7e49d1 100755 --- a/tests/interop/python-redis-typescript/run_tests.sh +++ b/tests/interop/python-redis-typescript/run_tests.sh @@ -24,6 +24,16 @@ check_redis() { echo -e "${GREEN}✅ Redis is running${NC}" } +# Activate the virtual environment +print_header "Activating Python Environment" +if [ -f "venv/bin/activate" ]; then + source venv/bin/activate + echo -e "${GREEN}✅ Virtual environment activated${NC}" +else + echo -e "${RED}❌ Virtual environment not found. Please run the setup script first: cd .. && ./setup.sh${NC}" + exit 1 +fi + # Function to check required dependencies check_dependencies() { print_header "Checking Required Dependencies" diff --git a/tests/interop/typescript-redis-python/run_tests.sh b/tests/interop/typescript-redis-python/run_tests.sh index 52b5dc7..b542b3e 100755 --- a/tests/interop/typescript-redis-python/run_tests.sh +++ b/tests/interop/typescript-redis-python/run_tests.sh @@ -24,6 +24,16 @@ check_redis() { echo -e "${GREEN}✅ Redis is running${NC}" } +# Activate the virtual environment +print_header "Activating Python Environment" +if [ -f "venv/bin/activate" ]; then + source venv/bin/activate + echo -e "${GREEN}✅ Virtual environment activated${NC}" +else + echo -e "${RED}❌ Virtual environment not found. Please run the setup script first: cd .. && ./setup.sh${NC}" + exit 1 +fi + # Function to check required dependencies check_dependencies() { print_header "Checking Required Dependencies" From 5b79dafa1ae220bae38912af417694bf1f4ce429 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 03:49:48 +0100 Subject: [PATCH 04/20] Add tsconfig.json and update TypeScript command to use npx ts-node --- tests/interop/python-redis-typescript/run_tests.sh | 2 +- tests/interop/python-redis-typescript/tsconfig.json | 13 +++++++++++++ tests/interop/typescript-redis-python/run_tests.sh | 2 +- tests/interop/typescript-redis-python/tsconfig.json | 13 +++++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/interop/python-redis-typescript/tsconfig.json create mode 100644 tests/interop/typescript-redis-python/tsconfig.json diff --git a/tests/interop/python-redis-typescript/run_tests.sh b/tests/interop/python-redis-typescript/run_tests.sh index b7e49d1..cc3e6d4 100755 --- a/tests/interop/python-redis-typescript/run_tests.sh +++ b/tests/interop/python-redis-typescript/run_tests.sh @@ -74,7 +74,7 @@ echo -e "${GREEN}✅ Python test completed successfully${NC}" # Run TypeScript verification print_header "Running TypeScript Verification" -if ! node --loader ts-node/esm verify_state.ts; then +if ! npx ts-node --esm verify_state.ts; then echo -e "${RED}❌ TypeScript verification failed${NC}" exit 1 fi diff --git a/tests/interop/python-redis-typescript/tsconfig.json b/tests/interop/python-redis-typescript/tsconfig.json new file mode 100644 index 0000000..4b26f9d --- /dev/null +++ b/tests/interop/python-redis-typescript/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/run_tests.sh b/tests/interop/typescript-redis-python/run_tests.sh index b542b3e..856ce96 100755 --- a/tests/interop/typescript-redis-python/run_tests.sh +++ b/tests/interop/typescript-redis-python/run_tests.sh @@ -66,7 +66,7 @@ check_dependencies # Run TypeScript state creation print_header "Running TypeScript State Creation" -if ! node --loader ts-node/esm create_state.ts; then +if ! npx ts-node --esm create_state.ts; then echo -e "${RED}❌ TypeScript state creation failed${NC}" exit 1 fi diff --git a/tests/interop/typescript-redis-python/tsconfig.json b/tests/interop/typescript-redis-python/tsconfig.json new file mode 100644 index 0000000..4b26f9d --- /dev/null +++ b/tests/interop/typescript-redis-python/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} \ No newline at end of file From 9f8c36e601a2f0ea48d7d174605fdfb234795077 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 03:51:09 +0100 Subject: [PATCH 05/20] Update TypeScript files to use .mjs extension --- tests/interop/python-redis-typescript/run_tests.sh | 2 +- .../{verify_state.ts => verify_state.mjs} | 0 .../{create_state.ts => create_state.mjs} | 0 tests/interop/typescript-redis-python/run_tests.sh | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename tests/interop/python-redis-typescript/{verify_state.ts => verify_state.mjs} (100%) rename tests/interop/typescript-redis-python/{create_state.ts => create_state.mjs} (100%) diff --git a/tests/interop/python-redis-typescript/run_tests.sh b/tests/interop/python-redis-typescript/run_tests.sh index cc3e6d4..06d57b1 100755 --- a/tests/interop/python-redis-typescript/run_tests.sh +++ b/tests/interop/python-redis-typescript/run_tests.sh @@ -74,7 +74,7 @@ echo -e "${GREEN}✅ Python test completed successfully${NC}" # Run TypeScript verification print_header "Running TypeScript Verification" -if ! npx ts-node --esm verify_state.ts; then +if ! node verify_state.mjs; then echo -e "${RED}❌ TypeScript verification failed${NC}" exit 1 fi diff --git a/tests/interop/python-redis-typescript/verify_state.ts b/tests/interop/python-redis-typescript/verify_state.mjs similarity index 100% rename from tests/interop/python-redis-typescript/verify_state.ts rename to tests/interop/python-redis-typescript/verify_state.mjs diff --git a/tests/interop/typescript-redis-python/create_state.ts b/tests/interop/typescript-redis-python/create_state.mjs similarity index 100% rename from tests/interop/typescript-redis-python/create_state.ts rename to tests/interop/typescript-redis-python/create_state.mjs diff --git a/tests/interop/typescript-redis-python/run_tests.sh b/tests/interop/typescript-redis-python/run_tests.sh index 856ce96..735bf30 100755 --- a/tests/interop/typescript-redis-python/run_tests.sh +++ b/tests/interop/typescript-redis-python/run_tests.sh @@ -66,7 +66,7 @@ check_dependencies # Run TypeScript state creation print_header "Running TypeScript State Creation" -if ! npx ts-node --esm create_state.ts; then +if ! node create_state.mjs; then echo -e "${RED}❌ TypeScript state creation failed${NC}" exit 1 fi From dfff7c2c84865a0ff1b3515528617338f68ba42e Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 03:53:27 +0100 Subject: [PATCH 06/20] Update imports to use dist folder for TypeScript --- tests/interop/python-redis-typescript/verify_state.mjs | 2 +- tests/interop/typescript-redis-python/create_state.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/interop/python-redis-typescript/verify_state.mjs b/tests/interop/python-redis-typescript/verify_state.mjs index 86b3a1b..145eb6c 100644 --- a/tests/interop/python-redis-typescript/verify_state.mjs +++ b/tests/interop/python-redis-typescript/verify_state.mjs @@ -6,7 +6,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; -import { BrowserState } from '../../../typescript/src/BrowserState.js'; +import { BrowserState } from '../../../typescript/dist/BrowserState.js'; import { chromium } from 'playwright'; // Get current file directory with ES modules diff --git a/tests/interop/typescript-redis-python/create_state.mjs b/tests/interop/typescript-redis-python/create_state.mjs index d7808f3..f779cb3 100644 --- a/tests/interop/typescript-redis-python/create_state.mjs +++ b/tests/interop/typescript-redis-python/create_state.mjs @@ -5,7 +5,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; -import { BrowserState } from '../../../typescript/src/BrowserState.js'; +import { BrowserState } from '../../../typescript/dist/BrowserState.js'; import { chromium } from 'playwright'; // Get current file directory with ES modules From 115705659b253e389276ada3690c7859aa9a663e Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 03:58:17 +0100 Subject: [PATCH 07/20] Fix TypeScript imports to use index.js --- tests/interop/python-redis-typescript/verify_state.mjs | 2 +- tests/interop/typescript-redis-python/create_state.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/interop/python-redis-typescript/verify_state.mjs b/tests/interop/python-redis-typescript/verify_state.mjs index 145eb6c..744bca2 100644 --- a/tests/interop/python-redis-typescript/verify_state.mjs +++ b/tests/interop/python-redis-typescript/verify_state.mjs @@ -6,7 +6,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; -import { BrowserState } from '../../../typescript/dist/BrowserState.js'; +import { BrowserState } from '../../../typescript/dist/index.js'; import { chromium } from 'playwright'; // Get current file directory with ES modules diff --git a/tests/interop/typescript-redis-python/create_state.mjs b/tests/interop/typescript-redis-python/create_state.mjs index f779cb3..958bb47 100644 --- a/tests/interop/typescript-redis-python/create_state.mjs +++ b/tests/interop/typescript-redis-python/create_state.mjs @@ -5,7 +5,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; -import { BrowserState } from '../../../typescript/dist/BrowserState.js'; +import { BrowserState } from '../../../typescript/dist/index.js'; import { chromium } from 'playwright'; // Get current file directory with ES modules From 8c11a2c3ab1076a06c0206851b4fdd354006b3e7 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 04:02:03 +0100 Subject: [PATCH 08/20] Add ioredis dependency installation to run_tests.sh scripts --- .../python-redis-typescript/debug_verify.mjs | 56 +++++++++ .../python-redis-typescript/package-lock.json | 111 ++++++++++++++++++ .../python-redis-typescript/package.json | 4 +- .../python-redis-typescript/run_tests.sh | 8 +- .../python-redis-typescript/test_import.mjs | 54 +++++++++ .../typescript-redis-python/run_tests.sh | 8 +- 6 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 tests/interop/python-redis-typescript/debug_verify.mjs create mode 100644 tests/interop/python-redis-typescript/test_import.mjs diff --git a/tests/interop/python-redis-typescript/debug_verify.mjs b/tests/interop/python-redis-typescript/debug_verify.mjs new file mode 100644 index 0000000..5fc69f8 --- /dev/null +++ b/tests/interop/python-redis-typescript/debug_verify.mjs @@ -0,0 +1,56 @@ +/** + * Debug script for the verification process + */ + +import { fileURLToPath } from 'url'; +import path from 'path'; +import fs from 'fs'; + +// Get current directory +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +console.log('🔍 Starting debug verification process...'); + +// Try importing modules +console.log('\nImporting modules...'); +try { + console.log('Importing BrowserState...'); + const { BrowserState } = await import('../../../typescript/dist/index.js'); + console.log('✅ Successfully imported BrowserState!'); + + console.log('\nImporting playwright...'); + const { chromium } = await import('playwright'); + console.log('✅ Successfully imported playwright!'); + + // Try to create a BrowserState instance + console.log('\nTrying to instantiate BrowserState with Redis...'); + const browserState = new BrowserState({ + userId: 'interop_test_user', + storageType: 'redis', + redisOptions: { + host: 'localhost', + port: 6379, + password: undefined, + db: 0, + keyPrefix: 'browserstate:', + ttl: 604800 // 7 days + } + }); + console.log('✅ Successfully created BrowserState instance!'); + + // Try to execute Redis-specific methods to see if they fail + console.log('\nList available sessions (this might fail if Redis connection issues)...'); + try { + const sessions = await browserState.listSessions(); + console.log(`✅ Found ${sessions.length} sessions: ${sessions.join(', ')}`); + } catch (err) { + console.error(`❌ Error listing sessions: ${err.message}`); + console.error('This might be due to Redis connectivity issues'); + } +} catch (err) { + console.error('❌ Error in debug process:'); + console.error(err); +} + +console.log('\n✨ Debug process complete!'); \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/package-lock.json b/tests/interop/python-redis-typescript/package-lock.json index d06f684..548be8b 100644 --- a/tests/interop/python-redis-typescript/package-lock.json +++ b/tests/interop/python-redis-typescript/package-lock.json @@ -14,6 +14,7 @@ "arg": "^4.1.3", "create-require": "^1.1.1", "diff": "^4.0.2", + "ioredis": "^5.6.0", "make-error": "^1.3.6", "playwright": "^1.51.1", "playwright-core": "^1.51.1", @@ -37,6 +38,12 @@ "node": ">=12" } }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -133,12 +140,47 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "license": "MIT" }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -162,12 +204,54 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/ioredis": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz", + "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "license": "ISC" }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/playwright": { "version": "1.51.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", @@ -198,6 +282,33 @@ "node": ">=18" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", diff --git a/tests/interop/python-redis-typescript/package.json b/tests/interop/python-redis-typescript/package.json index d6bfbbf..1381c30 100644 --- a/tests/interop/python-redis-typescript/package.json +++ b/tests/interop/python-redis-typescript/package.json @@ -16,6 +16,7 @@ "arg": "^4.1.3", "create-require": "^1.1.1", "diff": "^4.0.2", + "ioredis": "^5.6.0", "make-error": "^1.3.6", "playwright": "^1.51.1", "playwright-core": "^1.51.1", @@ -24,6 +25,5 @@ "undici-types": "^6.20.0", "v8-compile-cache-lib": "^3.0.1", "yn": "^3.1.1" - }, - "devDependencies": {} + } } diff --git a/tests/interop/python-redis-typescript/run_tests.sh b/tests/interop/python-redis-typescript/run_tests.sh index 06d57b1..0557fbf 100755 --- a/tests/interop/python-redis-typescript/run_tests.sh +++ b/tests/interop/python-redis-typescript/run_tests.sh @@ -34,7 +34,7 @@ else exit 1 fi -# Function to check required dependencies +# Function to check and install required dependencies check_dependencies() { print_header "Checking Required Dependencies" @@ -54,6 +54,12 @@ check_dependencies() { exit 1 fi + # Ensure ioredis is installed + if [ ! -f "node_modules/ioredis/package.json" ]; then + echo -e "${BLUE}Installing ioredis...${NC}" + npm install ioredis + fi + echo -e "${GREEN}✅ All dependencies installed${NC}" } diff --git a/tests/interop/python-redis-typescript/test_import.mjs b/tests/interop/python-redis-typescript/test_import.mjs new file mode 100644 index 0000000..13f0c7a --- /dev/null +++ b/tests/interop/python-redis-typescript/test_import.mjs @@ -0,0 +1,54 @@ +/** + * Simple test script to verify we can import BrowserState from the TypeScript package. + */ + +import { fileURLToPath } from 'url'; +import path from 'path'; +import fs from 'fs'; + +// Get current directory +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +console.log('🔍 Checking TypeScript package output...'); + +// Check if the dist directory exists +const distPath = path.join(__dirname, '../../../typescript/dist'); +console.log(`Checking if dist directory exists at: ${distPath}`); +console.log(`Dist directory exists: ${fs.existsSync(distPath)}`); + +// Check if the index.js file exists +const indexPath = path.join(distPath, 'index.js'); +console.log(`Checking if index.js exists at: ${indexPath}`); +console.log(`index.js exists: ${fs.existsSync(indexPath)}`); + +// List files in the dist directory +console.log('\nListing files in dist directory:'); +try { + const files = fs.readdirSync(distPath); + files.forEach(file => console.log(`- ${file}`)); +} catch (err) { + console.error(`Error listing files: ${err.message}`); +} + +console.log('\nAttempting to import BrowserState...'); +try { + const { BrowserState } = await import('../../../typescript/dist/index.js'); + console.log('✅ Successfully imported BrowserState!'); + console.log(`BrowserState type: ${typeof BrowserState}`); + + // Try to create a BrowserState instance + if (typeof BrowserState === 'function') { + console.log('\nTrying to instantiate BrowserState...'); + const browserState = new BrowserState({ + userId: 'test-user' + }); + console.log('✅ Successfully created BrowserState instance!'); + console.log(`Instance type: ${typeof browserState}`); + } +} catch (err) { + console.error('❌ Error importing BrowserState:'); + console.error(err); +} + +console.log('\n✨ Import test complete!'); \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/run_tests.sh b/tests/interop/typescript-redis-python/run_tests.sh index 735bf30..46e87f6 100755 --- a/tests/interop/typescript-redis-python/run_tests.sh +++ b/tests/interop/typescript-redis-python/run_tests.sh @@ -34,7 +34,7 @@ else exit 1 fi -# Function to check required dependencies +# Function to check and install required dependencies check_dependencies() { print_header "Checking Required Dependencies" @@ -54,6 +54,12 @@ check_dependencies() { exit 1 fi + # Ensure ioredis is installed + if [ ! -f "node_modules/ioredis/package.json" ]; then + echo -e "${BLUE}Installing ioredis...${NC}" + npm install ioredis + fi + echo -e "${GREEN}✅ All dependencies installed${NC}" } From 96976ec8e6dba4d42cf243c8fc9a48a421a41d2e Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 04:05:41 +0100 Subject: [PATCH 09/20] Fix localStorage detection in interop tests by using absolute paths and consistent URLs --- .../test_cross_language.py | 60 ++++++++++++------- .../python-redis-typescript/verify_state.mjs | 49 +++++++++++---- .../typescript-redis-python/create_state.mjs | 40 +++++++++---- .../typescript-redis-python/verify_state.py | 50 +++++++++------- 4 files changed, 131 insertions(+), 68 deletions(-) diff --git a/tests/interop/python-redis-typescript/test_cross_language.py b/tests/interop/python-redis-typescript/test_cross_language.py index c70536c..bd28e25 100644 --- a/tests/interop/python-redis-typescript/test_cross_language.py +++ b/tests/interop/python-redis-typescript/test_cross_language.py @@ -15,18 +15,16 @@ REDIS_URL = "redis://localhost:6379/0" REDIS_KEY_PREFIX = "browserstate:" -# Redis configuration matching TypeScript format - for reference only -REDIS_CONFIG_TS = { - "host": "localhost", - "port": 6379, - "password": None, - "db": 0, - "keyPrefix": "browserstate:", - "ttl": 604800 # 7 days -} +# Test session ID - must be the same across Python and TypeScript +SESSION_ID = "cross_language_test" +USER_ID = "interop_test_user" # Path to the test HTML file TEST_HTML_PATH = Path(__file__).parent.parent.parent.parent / "typescript" / "examples" / "shared" / "test.html" +TEST_URL = f"file://{TEST_HTML_PATH.absolute()}" + +# Debug mode - set to True to see browser UI during tests +DEBUG = False async def create_test_data(browser_state: BrowserState, session_id: str) -> None: """Create test data using Playwright and store it in Redis.""" @@ -40,7 +38,7 @@ async def create_test_data(browser_state: BrowserState, session_id: str) -> None # Launch browser with the mounted state browser = await p.chromium.launch_persistent_context( user_data_dir=state["path"], - headless=False + headless=not DEBUG ) try: @@ -48,8 +46,13 @@ async def create_test_data(browser_state: BrowserState, session_id: str) -> None page = await browser.new_page() # Navigate to the test HTML page - await page.goto(f"file://{TEST_HTML_PATH}") - print("📄 Loaded test page") + print(f"📄 Loading test page: {TEST_URL}") + await page.goto(TEST_URL) + + # Clear existing localStorage + await page.evaluate("""() => { + localStorage.clear(); + }""") # Add some test notes test_notes = [ @@ -75,6 +78,9 @@ async def create_test_data(browser_state: BrowserState, session_id: str) -> None }""") print(f"📝 Notes data: {notes_data}") + # Wait to ensure data is properly saved + await page.wait_for_timeout(1000) + finally: await browser.close() @@ -94,16 +100,19 @@ async def verify_test_data(browser_state: BrowserState, session_id: str) -> None # Launch browser with the mounted state browser = await p.chromium.launch_persistent_context( user_data_dir=state["path"], - headless=False + headless=not DEBUG ) try: # Create a new page page = await browser.new_page() - # Navigate to the test HTML page - await page.goto(f"file://{TEST_HTML_PATH}") - print("📄 Loaded test page") + # Navigate to the test HTML page - using the EXACT same URL + print(f"📄 Loading test page: {TEST_URL}") + await page.goto(TEST_URL) + + # Wait for the page to load completely + await page.wait_for_timeout(1000) # Get the notes data notes_data = await page.evaluate("""() => { @@ -117,6 +126,16 @@ async def verify_test_data(browser_state: BrowserState, session_id: str) -> None print(f" - {note['text']} ({note['timestamp']})") else: print("❌ No notes found in localStorage") + # Try to debug by looking at all localStorage items + storage_items = await page.evaluate("""() => { + const items = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + items[key] = localStorage.getItem(key); + } + return items; + }""") + print(f"Available localStorage items: {storage_items}") finally: await browser.close() @@ -137,19 +156,16 @@ async def main(): # Initialize browser state with Redis storage options = BrowserStateOptions( - user_id="interop_test_user", + user_id=USER_ID, redis_options=redis_options ) browser_state = BrowserState(options) - # Test session ID - session_id = "cross_language_test" - # Create test data with Python - await create_test_data(browser_state, session_id) + await create_test_data(browser_state, SESSION_ID) # Verify the data can be read - await verify_test_data(browser_state, session_id) + await verify_test_data(browser_state, SESSION_ID) print("\n✨ Test completed successfully!") diff --git a/tests/interop/python-redis-typescript/verify_state.mjs b/tests/interop/python-redis-typescript/verify_state.mjs index 744bca2..565b9df 100644 --- a/tests/interop/python-redis-typescript/verify_state.mjs +++ b/tests/interop/python-redis-typescript/verify_state.mjs @@ -13,7 +13,14 @@ import { chromium } from 'playwright'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// Redis configuration matching Python example +// Test constants - must match Python test +const SESSION_ID = 'cross_language_test'; +const USER_ID = 'interop_test_user'; + +// Debug mode - set to true to see browser UI +const DEBUG = false; + +// Redis configuration const REDIS_CONFIG = { host: 'localhost', port: 6379, @@ -23,8 +30,9 @@ const REDIS_CONFIG = { ttl: 604800 // 7 days }; -// Path to the test HTML file -const TEST_HTML_PATH = path.join(__dirname, '../../../typescript/examples/shared/test.html'); +// Path to the test HTML file - using absolute path to ensure same origin +const TEST_HTML_PATH = path.resolve(path.join(__dirname, '../../../typescript/examples/shared/test.html')); +const TEST_URL = `file://${TEST_HTML_PATH}`; async function verifyPythonState() { console.log('🚀 Starting TypeScript Verification of Python State\n'); @@ -33,33 +41,33 @@ async function verifyPythonState() { // Initialize BrowserState with Redis storage console.log('🔧 Creating BrowserState with Redis storage...'); const browserState = new BrowserState({ - userId: 'interop_test_user', + userId: USER_ID, storageType: 'redis', redisOptions: REDIS_CONFIG }); - // Test session ID - const sessionId = 'cross_language_test'; - // Mount the session - console.log(`\n📥 Mounting session: ${sessionId}`); - const userDataDir = await browserState.mount(sessionId); + console.log(`\n📥 Mounting session: ${SESSION_ID}`); + const userDataDir = await browserState.mount(SESSION_ID); console.log(`📂 Mounted at: ${userDataDir}`); // Launch browser with the mounted state console.log('\n🌐 Launching browser with Playwright...'); const browser = await chromium.launchPersistentContext(userDataDir, { - headless: false + headless: !DEBUG }); try { // Create a new page const page = await browser.newPage(); - // Navigate to the test HTML page - console.log('\n📄 Loading test page...'); - await page.goto(`file://${TEST_HTML_PATH}`); + // Navigate to the test HTML page - using exact same URL as Python + console.log(`\n📄 Loading test page: ${TEST_URL}`); + await page.goto(TEST_URL); console.log('✅ Test page loaded'); + + // Wait for the page to load completely + await page.waitForTimeout(1000); // Get the notes data console.log('\n📝 Reading notes from localStorage...'); @@ -81,6 +89,21 @@ async function verifyPythonState() { console.log(`\n✅ Found ${pythonNotes.length} Python-created notes`); } else { console.log('\n❌ No notes found in localStorage'); + + // Debug: check all localStorage items + const allItems = await page.evaluate(() => { + const items = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + items[key] = localStorage.getItem(key); + } + return items; + }); + console.log('Available localStorage items:', allItems); + + // Try to examine the HTML structure + const html = await page.content(); + console.log(`Page HTML (first 200 chars): ${html.substring(0, 200)}...`); } } finally { diff --git a/tests/interop/typescript-redis-python/create_state.mjs b/tests/interop/typescript-redis-python/create_state.mjs index 958bb47..94427ac 100644 --- a/tests/interop/typescript-redis-python/create_state.mjs +++ b/tests/interop/typescript-redis-python/create_state.mjs @@ -12,6 +12,13 @@ import { chromium } from 'playwright'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +// Test constants - must match Python test +const SESSION_ID = 'typescript_to_python_test'; +const USER_ID = 'interop_test_user'; + +// Debug mode - set to true to see browser UI +const DEBUG = false; + // Redis configuration const REDIS_CONFIG = { host: 'localhost', @@ -22,8 +29,9 @@ const REDIS_CONFIG = { ttl: 604800 // 7 days }; -// Path to the test HTML file -const TEST_HTML_PATH = path.join(__dirname, '../../../typescript/examples/shared/test.html'); +// Path to the test HTML file - using absolute path to ensure same origin +const TEST_HTML_PATH = path.resolve(path.join(__dirname, '../../../typescript/examples/shared/test.html')); +const TEST_URL = `file://${TEST_HTML_PATH}`; async function createTestData() { console.log('🚀 Starting TypeScript State Creation\n'); @@ -32,38 +40,43 @@ async function createTestData() { // Initialize BrowserState with Redis storage console.log('🔧 Creating BrowserState with Redis storage...'); const browserState = new BrowserState({ - userId: 'interop_test_user', + userId: USER_ID, storageType: 'redis', redisOptions: REDIS_CONFIG }); - // Test session ID - const sessionId = 'typescript_to_python_test'; - // List existing sessions console.log('\n📋 Listing existing sessions...'); const sessions = await browserState.listSessions(); console.log(`Found ${sessions.length} session(s): ${sessions.join(', ')}`); // Mount the session - console.log(`\n📥 Mounting session: ${sessionId}`); - const userDataDir = await browserState.mount(sessionId); + console.log(`\n📥 Mounting session: ${SESSION_ID}`); + const userDataDir = await browserState.mount(SESSION_ID); console.log(`📂 Mounted at: ${userDataDir}`); // Launch browser with the mounted state console.log('\n🌐 Launching browser with Playwright...'); const browser = await chromium.launchPersistentContext(userDataDir, { - headless: false + headless: !DEBUG }); try { // Create a new page const page = await browser.newPage(); - // Navigate to the test HTML page - console.log('\n📄 Loading test page...'); - await page.goto(`file://${TEST_HTML_PATH}`); + // Navigate to the test HTML page - using exact same URL as Python verification + console.log(`\n📄 Loading test page: ${TEST_URL}`); + await page.goto(TEST_URL); console.log('✅ Test page loaded'); + + // Clear existing localStorage to start fresh + await page.evaluate(() => { + localStorage.clear(); + }); + + // Wait for the page to load completely + await page.waitForTimeout(1000); // Add notes to localStorage console.log('\n📝 Adding notes to localStorage...'); @@ -90,6 +103,9 @@ async function createTestData() { return localStorage.getItem('notes'); }); console.log(`📝 Notes data: ${notesData}`); + + // Wait to ensure data is properly saved + await page.waitForTimeout(1000); } finally { await browser.close(); diff --git a/tests/interop/typescript-redis-python/verify_state.py b/tests/interop/typescript-redis-python/verify_state.py index d5b3cd7..cb138ac 100644 --- a/tests/interop/typescript-redis-python/verify_state.py +++ b/tests/interop/typescript-redis-python/verify_state.py @@ -15,18 +15,16 @@ REDIS_URL = "redis://localhost:6379/0" REDIS_KEY_PREFIX = "browserstate:" -# Redis configuration matching TypeScript format - for reference only -REDIS_CONFIG_TS = { - "host": "localhost", - "port": 6379, - "password": None, - "db": 0, - "keyPrefix": "browserstate:", - "ttl": 604800 # 7 days -} +# Test constants - must match TypeScript test +SESSION_ID = "typescript_to_python_test" +USER_ID = "interop_test_user" -# Path to the test HTML file +# Debug mode - set to True to see browser UI +DEBUG = False + +# Path to the test HTML file - using absolute path to ensure same origin TEST_HTML_PATH = Path(__file__).parent.parent.parent.parent / "typescript" / "examples" / "shared" / "test.html" +TEST_URL = f"file://{TEST_HTML_PATH.absolute()}" async def verify_typescript_state(): """Verify the browser state created by TypeScript.""" @@ -40,40 +38,40 @@ async def verify_typescript_state(): # Initialize browser state with Redis storage options = BrowserStateOptions( - user_id="interop_test_user", + user_id=USER_ID, redis_options=redis_options ) browser_state = BrowserState(options) - # Test session ID - session_id = "typescript_to_python_test" - # List available sessions sessions = browser_state.list_sessions() print(f"📋 Available sessions: {sessions}") - if session_id not in sessions: - print(f"❌ Session '{session_id}' not found") + if SESSION_ID not in sessions: + print(f"❌ Session '{SESSION_ID}' not found") return # Mount the session - state = browser_state.mount_session(session_id) + state = browser_state.mount_session(SESSION_ID) print(f"📂 Mounted session at: {state['path']}") async with async_playwright() as p: # Launch browser with the mounted state browser = await p.chromium.launch_persistent_context( user_data_dir=state["path"], - headless=False + headless=not DEBUG ) try: # Create a new page page = await browser.new_page() - # Navigate to the test HTML page - await page.goto(f"file://{TEST_HTML_PATH}") - print("📄 Loaded test page") + # Navigate to the test HTML page - using the EXACT same URL + print(f"📄 Loading test page: {TEST_URL}") + await page.goto(TEST_URL) + + # Wait for the page to load completely + await page.wait_for_timeout(1000) # Get the notes data notes_data = await page.evaluate("""() => { @@ -94,6 +92,16 @@ async def verify_typescript_state(): print("❌ No TypeScript-created notes found") else: print("❌ No notes found in localStorage") + # Try to debug by looking at all localStorage items + storage_items = await page.evaluate("""() => { + const items = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + items[key] = localStorage.getItem(key); + } + return items; + }""") + print(f"Available localStorage items: {storage_items}") finally: await browser.close() From d55978a0ace1743d18acad0bd3df0027fb923639 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 04:08:49 +0100 Subject: [PATCH 10/20] Add explicit assertions to fail immediately if any verification step fails --- .../test_cross_language.py | 23 +++++++++++++++++- .../python-redis-typescript/verify_state.mjs | 17 +++++++++++-- .../typescript-redis-python/create_state.mjs | 21 ++++++++++++++-- .../typescript-redis-python/verify_state.py | 24 ++++++++++++------- 4 files changed, 72 insertions(+), 13 deletions(-) diff --git a/tests/interop/python-redis-typescript/test_cross_language.py b/tests/interop/python-redis-typescript/test_cross_language.py index bd28e25..93ea3be 100644 --- a/tests/interop/python-redis-typescript/test_cross_language.py +++ b/tests/interop/python-redis-typescript/test_cross_language.py @@ -7,6 +7,7 @@ import os import json import asyncio +import sys from pathlib import Path from playwright.async_api import async_playwright from browserstate import BrowserState, BrowserStateOptions, RedisStorage @@ -26,6 +27,11 @@ # Debug mode - set to True to see browser UI during tests DEBUG = False +def fail_test(message): + """Fail the test with a clear error message.""" + print(f"\n❌ TEST FAILED: {message}") + sys.exit(1) + async def create_test_data(browser_state: BrowserState, session_id: str) -> None: """Create test data using Playwright and store it in Redis.""" print(f"\n🔧 Creating test data in session: {session_id}") @@ -72,12 +78,20 @@ async def create_test_data(browser_state: BrowserState, session_id: str) -> None }""") print(f"✅ Added {notes_count} notes") + # Assert that notes were actually added + if notes_count != len(test_notes): + fail_test(f"Expected {len(test_notes)} notes, but found {notes_count}") + # Get the notes data for verification notes_data = await page.evaluate("""() => { return localStorage.getItem('notes'); }""") print(f"📝 Notes data: {notes_data}") + # Verify notes data is not empty + if not notes_data: + fail_test("Notes data is empty") + # Wait to ensure data is properly saved await page.wait_for_timeout(1000) @@ -124,8 +138,14 @@ async def verify_test_data(browser_state: BrowserState, session_id: str) -> None print(f"📝 Found {len(notes)} notes:") for note in notes: print(f" - {note['text']} ({note['timestamp']})") + + # Verify that we have Python notes + python_notes = [note for note in notes if note['text'].startswith('Python created note')] + if not python_notes: + fail_test("No Python-created notes found in localStorage") + + print(f"✅ Found {len(python_notes)} Python-created notes") else: - print("❌ No notes found in localStorage") # Try to debug by looking at all localStorage items storage_items = await page.evaluate("""() => { const items = {}; @@ -136,6 +156,7 @@ async def verify_test_data(browser_state: BrowserState, session_id: str) -> None return items; }""") print(f"Available localStorage items: {storage_items}") + fail_test("No notes found in localStorage") finally: await browser.close() diff --git a/tests/interop/python-redis-typescript/verify_state.mjs b/tests/interop/python-redis-typescript/verify_state.mjs index 565b9df..3024ae6 100644 --- a/tests/interop/python-redis-typescript/verify_state.mjs +++ b/tests/interop/python-redis-typescript/verify_state.mjs @@ -34,6 +34,12 @@ const REDIS_CONFIG = { const TEST_HTML_PATH = path.resolve(path.join(__dirname, '../../../typescript/examples/shared/test.html')); const TEST_URL = `file://${TEST_HTML_PATH}`; +// Function to fail the test with a clear error message +function failTest(message) { + console.error(`\n❌ TEST FAILED: ${message}`); + process.exit(1); +} + async function verifyPythonState() { console.log('🚀 Starting TypeScript Verification of Python State\n'); @@ -86,6 +92,11 @@ async function verifyPythonState() { const pythonNotes = notes.filter((note) => note.text.startsWith('Python created note') ); + + if (pythonNotes.length === 0) { + failTest('No Python-created notes found in localStorage'); + } + console.log(`\n✅ Found ${pythonNotes.length} Python-created notes`); } else { console.log('\n❌ No notes found in localStorage'); @@ -104,6 +115,9 @@ async function verifyPythonState() { // Try to examine the HTML structure const html = await page.content(); console.log(`Page HTML (first 200 chars): ${html.substring(0, 200)}...`); + + // Fail the test + failTest('No notes found in localStorage'); } } finally { @@ -118,8 +132,7 @@ async function verifyPythonState() { console.log('\n✨ Verification complete!'); } catch (error) { - console.error('\n❌ Error during verification:', error instanceof Error ? error.message : String(error)); - process.exit(1); + failTest(`Error during verification: ${error instanceof Error ? error.message : String(error)}`); } } diff --git a/tests/interop/typescript-redis-python/create_state.mjs b/tests/interop/typescript-redis-python/create_state.mjs index 94427ac..dbb2672 100644 --- a/tests/interop/typescript-redis-python/create_state.mjs +++ b/tests/interop/typescript-redis-python/create_state.mjs @@ -33,6 +33,12 @@ const REDIS_CONFIG = { const TEST_HTML_PATH = path.resolve(path.join(__dirname, '../../../typescript/examples/shared/test.html')); const TEST_URL = `file://${TEST_HTML_PATH}`; +// Function to fail the test with a clear error message +function failTest(message) { + console.error(`\n❌ TEST FAILED: ${message}`); + process.exit(1); +} + async function createTestData() { console.log('🚀 Starting TypeScript State Creation\n'); @@ -96,12 +102,24 @@ async function createTestData() { const notesCount = await page.evaluate(() => { return JSON.parse(localStorage.getItem('notes') || '[]').length; }); + + // Verify notes were added successfully + if (notesCount !== testNotes.length) { + failTest(`Expected ${testNotes.length} notes, but found ${notesCount}`); + } + console.log(`✅ Added ${notesCount} notes`); // Get the notes data for verification const notesData = await page.evaluate(() => { return localStorage.getItem('notes'); }); + + // Verify notes data is not empty + if (!notesData) { + failTest('Notes data is empty'); + } + console.log(`📝 Notes data: ${notesData}`); // Wait to ensure data is properly saved @@ -119,8 +137,7 @@ async function createTestData() { console.log('\n✨ State creation complete!'); } catch (error) { - console.error('\n❌ Error during state creation:', error instanceof Error ? error.message : String(error)); - process.exit(1); + failTest(`Error during state creation: ${error instanceof Error ? error.message : String(error)}`); } } diff --git a/tests/interop/typescript-redis-python/verify_state.py b/tests/interop/typescript-redis-python/verify_state.py index cb138ac..f1ea415 100644 --- a/tests/interop/typescript-redis-python/verify_state.py +++ b/tests/interop/typescript-redis-python/verify_state.py @@ -7,6 +7,7 @@ import os import json import asyncio +import sys from pathlib import Path from playwright.async_api import async_playwright from browserstate import BrowserState, BrowserStateOptions, RedisStorage @@ -26,6 +27,11 @@ TEST_HTML_PATH = Path(__file__).parent.parent.parent.parent / "typescript" / "examples" / "shared" / "test.html" TEST_URL = f"file://{TEST_HTML_PATH.absolute()}" +def fail_test(message): + """Fail the test with a clear error message.""" + print(f"\n❌ TEST FAILED: {message}") + sys.exit(1) + async def verify_typescript_state(): """Verify the browser state created by TypeScript.""" print("\n🔍 Verifying TypeScript-created browser state") @@ -48,8 +54,7 @@ async def verify_typescript_state(): print(f"📋 Available sessions: {sessions}") if SESSION_ID not in sessions: - print(f"❌ Session '{SESSION_ID}' not found") - return + fail_test(f"Session '{SESSION_ID}' not found") # Mount the session state = browser_state.mount_session(SESSION_ID) @@ -86,12 +91,11 @@ async def verify_typescript_state(): # Verify the notes were created by TypeScript typescript_notes = [note for note in notes if note['text'].startswith('TypeScript created note')] - if typescript_notes: - print(f"✅ Found {len(typescript_notes)} TypeScript-created notes") - else: - print("❌ No TypeScript-created notes found") + if not typescript_notes: + fail_test("No TypeScript-created notes found") + + print(f"✅ Found {len(typescript_notes)} TypeScript-created notes") else: - print("❌ No notes found in localStorage") # Try to debug by looking at all localStorage items storage_items = await page.evaluate("""() => { const items = {}; @@ -102,6 +106,7 @@ async def verify_typescript_state(): return items; }""") print(f"Available localStorage items: {storage_items}") + fail_test("No notes found in localStorage") finally: await browser.close() @@ -111,4 +116,7 @@ async def verify_typescript_state(): print("✅ Verification complete") if __name__ == "__main__": - asyncio.run(verify_typescript_state()) \ No newline at end of file + try: + asyncio.run(verify_typescript_state()) + except Exception as e: + fail_test(f"Unexpected error: {e}") \ No newline at end of file From e77f42d6192276df687bf7b6c09a75f097bcdc85 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 04:15:26 +0100 Subject: [PATCH 11/20] Fix: Updated RedisStorage to use ZIP format to match TypeScript implementation for interoperability --- python/browserstate/storage/redis_storage.py | 154 +++++++++++++++---- 1 file changed, 121 insertions(+), 33 deletions(-) diff --git a/python/browserstate/storage/redis_storage.py b/python/browserstate/storage/redis_storage.py index 6569ad9..7e40bfb 100644 --- a/python/browserstate/storage/redis_storage.py +++ b/python/browserstate/storage/redis_storage.py @@ -1,9 +1,12 @@ import os import io -import tarfile +import zipfile import tempfile import shutil import logging +import base64 +import json +import time from typing import List import redis # Requires: pip install redis @@ -13,7 +16,7 @@ class RedisStorage(StorageProvider): """ Storage provider implementation that uses Redis to store browser sessions - as compressed tar archives. + as compressed ZIP archives to match the TypeScript implementation. """ def __init__(self, @@ -27,17 +30,28 @@ def __init__(self, key_prefix: Prefix to use for keys in Redis. """ self.redis_client = redis.Redis.from_url(redis_url) - self.key_prefix = key_prefix + + # Remove trailing colon if it exists to prevent double colons + self.key_prefix = key_prefix.rstrip(':') + logging.info(f"Redis storage initialized with prefix: {self.key_prefix}") def _get_key(self, user_id: str, session_id: str) -> str: """ Generate a Redis key for a given user and session. """ - return f"{self.key_prefix}:{user_id}:{session_id}" + # Create key with format "prefix:userId:sessionId" (ensure no double colons) + key = f"{self.key_prefix}:{user_id}:{session_id}" + return key + + def _get_metadata_key(self, user_id: str, session_id: str) -> str: + """ + Generate a Redis key for session metadata. + """ + return f"{self.key_prefix}:{user_id}:{session_id}:metadata" def _get_temp_path(self, user_id: str, session_id: str) -> str: """ - Get a temporary path for a session similar to S3Storage implementation. + Get a temporary path for a session. Args: user_id: User identifier. @@ -50,24 +64,9 @@ def _get_temp_path(self, user_id: str, session_id: str) -> str: os.makedirs(temp_dir, exist_ok=True) return os.path.join(temp_dir, session_id) - def _safe_extract(self, tar_obj: tarfile.TarFile, path: str) -> None: - """ - Safely extract tar file to prevent path traversal vulnerabilities. - """ - def is_within_directory(directory: str, target: str) -> bool: - abs_directory = os.path.abspath(directory) - abs_target = os.path.abspath(target) - return os.path.commonprefix([abs_directory, abs_target]) == abs_directory - - for member in tar_obj.getmembers(): - member_path = os.path.join(path, member.name) - if not is_within_directory(path, member_path): - raise Exception("Attempted Path Traversal in Tar File") - tar_obj.extractall(path) - def download(self, user_id: str, session_id: str) -> str: """ - Downloads a browser session from Redis, decompresses it, and writes it + Downloads a browser session from Redis, extracts the ZIP archive, and writes it to a local temporary directory. Args: @@ -78,7 +77,12 @@ def download(self, user_id: str, session_id: str) -> str: Path to the local directory containing the session data. """ key = self._get_key(user_id, session_id) - tar_bytes = self.redis_client.get(key) + metadata_key = self._get_metadata_key(user_id, session_id) + + logging.info(f"Looking up session data at Redis key: {key}") + + # Get base64-encoded zip data from Redis + zip_data_base64 = self.redis_client.get(key) target_path = self._get_temp_path(user_id, session_id) @@ -86,14 +90,38 @@ def download(self, user_id: str, session_id: str) -> str: shutil.rmtree(target_path) os.makedirs(target_path, exist_ok=True) - if tar_bytes is None: + if zip_data_base64 is None: # No session found; return an empty directory. + logging.info(f"No session found at key: {key}") return target_path try: - tar_stream = io.BytesIO(tar_bytes) - with tarfile.open(fileobj=tar_stream, mode="r:gz") as tar: - self._safe_extract(tar, target_path) + # Decode base64 data + logging.info(f"Found session data of size: {len(zip_data_base64)} bytes") + zip_data = base64.b64decode(zip_data_base64) + logging.info(f"Decoded base64 data of size: {len(zip_data)} bytes") + + # Create temporary zip file + zip_file_path = os.path.join( + tempfile.gettempdir(), + f"{user_id}-{session_id}-{os.getpid()}.zip" + ) + + # Write zip data to temporary file + with open(zip_file_path, 'wb') as f: + f.write(zip_data) + + logging.info(f"Extracting ZIP file to: {target_path}") + + # Extract zip file to target directory + with zipfile.ZipFile(zip_file_path, 'r') as zip_ref: + zip_ref.extractall(target_path) + + # Clean up temporary zip file + os.remove(zip_file_path) + + logging.info(f"Extracted session data to {target_path}") + except Exception as e: logging.error(f"Error extracting session from Redis: {e}") raise @@ -102,7 +130,8 @@ def download(self, user_id: str, session_id: str) -> str: def upload(self, user_id: str, session_id: str, file_path: str) -> None: """ - Compresses the session directory into a tar.gz archive and uploads it to Redis. + Compresses the session directory into a ZIP archive and uploads it to Redis. + Uses base64 encoding to match TypeScript implementation. Args: user_id: User identifier. @@ -110,14 +139,64 @@ def upload(self, user_id: str, session_id: str, file_path: str) -> None: file_path: Path to the local directory containing session data. """ key = self._get_key(user_id, session_id) - tar_stream = io.BytesIO() + metadata_key = self._get_metadata_key(user_id, session_id) + + logging.info(f"Uploading session to Redis key: {key}") + + # Create temporary zip file + zip_file_path = os.path.join( + tempfile.gettempdir(), + f"{user_id}-{session_id}-{os.getpid()}.zip" + ) + try: - with tarfile.open(fileobj=tar_stream, mode="w:gz") as tar: - tar.add(file_path, arcname=os.path.basename(file_path)) - tar_bytes = tar_stream.getvalue() - self.redis_client.set(key, tar_bytes) + # Create ZIP archive with maximum compression + with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as zipf: + for root, dirs, files in os.walk(file_path): + for file in files: + file_path_full = os.path.join(root, file) + try: + arcname = os.path.relpath(file_path_full, file_path) + zipf.write(file_path_full, arcname) + except Exception as e: + logging.warning(f"Error adding file to ZIP: {file_path_full} - {e}") + + # Read zip file as binary + with open(zip_file_path, 'rb') as f: + zip_bytes = f.read() + + # Get file size for logging + zip_size = os.path.getsize(zip_file_path) + logging.info(f"Created ZIP archive of size: {zip_size} bytes") + + # Convert to base64 for Redis storage (matching TypeScript implementation) + zip_base64 = base64.b64encode(zip_bytes) + logging.info(f"Base64 encoded data size: {len(zip_base64)} bytes") + + # Store in Redis + self.redis_client.set(key, zip_base64) + + # Create metadata (matching TypeScript metadata format) + metadata = { + "timestamp": time.time() * 1000, # Current time in milliseconds + "version": "2.0", # Match TypeScript version + } + + # Store metadata in Redis + self.redis_client.set(metadata_key, json.dumps(metadata)) + + # Clean up temporary zip file + os.remove(zip_file_path) + + logging.info(f"Successfully uploaded session {session_id} to Redis at key: {key}") + except Exception as e: logging.error(f"Error uploading session to Redis: {e}") + + # Clean up temporary zip file if it exists + if os.path.exists(zip_file_path): + os.remove(zip_file_path) + raise def list_sessions(self, user_id: str) -> List[str]: @@ -131,6 +210,8 @@ def list_sessions(self, user_id: str) -> List[str]: List of session identifiers. """ pattern = f"{self.key_prefix}:{user_id}:*" + logging.info(f"Listing sessions with pattern: {pattern}") + try: keys = self.redis_client.keys(pattern) session_ids = [] @@ -139,6 +220,8 @@ def list_sessions(self, user_id: str) -> List[str]: parts = key_str.split(':') if len(parts) == 3: session_ids.append(parts[2]) + + logging.info(f"Found {len(session_ids)} sessions for user {user_id}") return session_ids except Exception as e: logging.error(f"Error listing sessions in Redis: {e}") @@ -153,8 +236,13 @@ def delete_session(self, user_id: str, session_id: str) -> None: session_id: Session identifier. """ key = self._get_key(user_id, session_id) + metadata_key = self._get_metadata_key(user_id, session_id) + logging.info(f"Deleting session at keys: {key}, {metadata_key}") + try: - self.redis_client.delete(key) + # Delete both session data and metadata + self.redis_client.delete(key, metadata_key) + logging.info(f"Successfully deleted session {session_id}") except Exception as e: logging.error(f"Error deleting session from Redis: {e}") raise From 843586a34fdc44f9e0ec38de902360e20f4e12fe Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 04:39:23 +0100 Subject: [PATCH 12/20] Chore: Added .gitignore for virtual environments and Redis dump files --- .gitignore | 52 +++- .../python-redis-typescript/venv/.gitignore | 4 - .../venv/bin/Activate.ps1 | 247 ------------------ .../python-redis-typescript/venv/bin/activate | 69 ----- .../venv/bin/activate.csh | 26 -- .../venv/bin/activate.fish | 69 ----- .../python-redis-typescript/venv/bin/pip | 8 - .../python-redis-typescript/venv/bin/pip3 | 8 - .../python-redis-typescript/venv/bin/pip3.11 | 8 - .../python-redis-typescript/venv/bin/python | 1 - .../python-redis-typescript/venv/bin/python3 | 1 - .../venv/bin/python3.11 | 1 - .../python-redis-typescript/venv/pyvenv.cfg | 5 - .../typescript-redis-python/venv/.gitignore | 4 - .../venv/bin/Activate.ps1 | 247 ------------------ .../typescript-redis-python/venv/bin/activate | 69 ----- .../venv/bin/activate.csh | 26 -- .../venv/bin/activate.fish | 69 ----- .../typescript-redis-python/venv/bin/pip | 8 - .../typescript-redis-python/venv/bin/pip3 | 8 - .../typescript-redis-python/venv/bin/pip3.11 | 8 - .../typescript-redis-python/venv/bin/python | 1 - .../typescript-redis-python/venv/bin/python3 | 1 - .../venv/bin/python3.11 | 1 - .../typescript-redis-python/venv/pyvenv.cfg | 5 - 25 files changed, 51 insertions(+), 895 deletions(-) delete mode 100644 tests/interop/python-redis-typescript/venv/.gitignore delete mode 100644 tests/interop/python-redis-typescript/venv/bin/Activate.ps1 delete mode 100644 tests/interop/python-redis-typescript/venv/bin/activate delete mode 100644 tests/interop/python-redis-typescript/venv/bin/activate.csh delete mode 100644 tests/interop/python-redis-typescript/venv/bin/activate.fish delete mode 100755 tests/interop/python-redis-typescript/venv/bin/pip delete mode 100755 tests/interop/python-redis-typescript/venv/bin/pip3 delete mode 100755 tests/interop/python-redis-typescript/venv/bin/pip3.11 delete mode 120000 tests/interop/python-redis-typescript/venv/bin/python delete mode 120000 tests/interop/python-redis-typescript/venv/bin/python3 delete mode 120000 tests/interop/python-redis-typescript/venv/bin/python3.11 delete mode 100644 tests/interop/python-redis-typescript/venv/pyvenv.cfg delete mode 100644 tests/interop/typescript-redis-python/venv/.gitignore delete mode 100644 tests/interop/typescript-redis-python/venv/bin/Activate.ps1 delete mode 100644 tests/interop/typescript-redis-python/venv/bin/activate delete mode 100644 tests/interop/typescript-redis-python/venv/bin/activate.csh delete mode 100644 tests/interop/typescript-redis-python/venv/bin/activate.fish delete mode 100755 tests/interop/typescript-redis-python/venv/bin/pip delete mode 100755 tests/interop/typescript-redis-python/venv/bin/pip3 delete mode 100755 tests/interop/typescript-redis-python/venv/bin/pip3.11 delete mode 120000 tests/interop/typescript-redis-python/venv/bin/python delete mode 120000 tests/interop/typescript-redis-python/venv/bin/python3 delete mode 120000 tests/interop/typescript-redis-python/venv/bin/python3.11 delete mode 100644 tests/interop/typescript-redis-python/venv/pyvenv.cfg diff --git a/.gitignore b/.gitignore index 299e4bc..1a9ddbc 100644 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,54 @@ typescript/examples/google/service-account.json typescript/examples/s3/aws-credentials.json # S3 Configuration -typescript/examples/s3/config.json \ No newline at end of file +typescript/examples/s3/config.json + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +dist/ +build/ +*.egg-info/ + +# Virtual environments +venv/ +env/ +ENV/ +.venv/ +.env/ + +# Node.js dependencies +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo + +# Testing +.coverage +coverage/ +htmlcov/ + +# Redis +dump.rdb +*.rdb + +# OS specific +.DS_Store +Thumbs.db + +# Local configuration +.env +.env.local +.env.*.local \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/venv/.gitignore b/tests/interop/python-redis-typescript/venv/.gitignore deleted file mode 100644 index 61d97f0..0000000 --- a/tests/interop/python-redis-typescript/venv/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/venv/bin/Activate.ps1 b/tests/interop/python-redis-typescript/venv/bin/Activate.ps1 deleted file mode 100644 index b49d77b..0000000 --- a/tests/interop/python-redis-typescript/venv/bin/Activate.ps1 +++ /dev/null @@ -1,247 +0,0 @@ -<# -.Synopsis -Activate a Python virtual environment for the current PowerShell session. - -.Description -Pushes the python executable for a virtual environment to the front of the -$Env:PATH environment variable and sets the prompt to signify that you are -in a Python virtual environment. Makes use of the command line switches as -well as the `pyvenv.cfg` file values present in the virtual environment. - -.Parameter VenvDir -Path to the directory that contains the virtual environment to activate. The -default value for this is the parent of the directory that the Activate.ps1 -script is located within. - -.Parameter Prompt -The prompt prefix to display when this virtual environment is activated. By -default, this prompt is the name of the virtual environment folder (VenvDir) -surrounded by parentheses and followed by a single space (ie. '(.venv) '). - -.Example -Activate.ps1 -Activates the Python virtual environment that contains the Activate.ps1 script. - -.Example -Activate.ps1 -Verbose -Activates the Python virtual environment that contains the Activate.ps1 script, -and shows extra information about the activation as it executes. - -.Example -Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv -Activates the Python virtual environment located in the specified location. - -.Example -Activate.ps1 -Prompt "MyPython" -Activates the Python virtual environment that contains the Activate.ps1 script, -and prefixes the current prompt with the specified string (surrounded in -parentheses) while the virtual environment is active. - -.Notes -On Windows, it may be required to enable this Activate.ps1 script by setting the -execution policy for the user. You can do this by issuing the following PowerShell -command: - -PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - -For more information on Execution Policies: -https://go.microsoft.com/fwlink/?LinkID=135170 - -#> -Param( - [Parameter(Mandatory = $false)] - [String] - $VenvDir, - [Parameter(Mandatory = $false)] - [String] - $Prompt -) - -<# Function declarations --------------------------------------------------- #> - -<# -.Synopsis -Remove all shell session elements added by the Activate script, including the -addition of the virtual environment's Python executable from the beginning of -the PATH variable. - -.Parameter NonDestructive -If present, do not remove this function from the global namespace for the -session. - -#> -function global:deactivate ([switch]$NonDestructive) { - # Revert to original values - - # The prior prompt: - if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { - Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt - Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT - } - - # The prior PYTHONHOME: - if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { - Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME - Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME - } - - # The prior PATH: - if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { - Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH - Remove-Item -Path Env:_OLD_VIRTUAL_PATH - } - - # Just remove the VIRTUAL_ENV altogether: - if (Test-Path -Path Env:VIRTUAL_ENV) { - Remove-Item -Path env:VIRTUAL_ENV - } - - # Just remove VIRTUAL_ENV_PROMPT altogether. - if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { - Remove-Item -Path env:VIRTUAL_ENV_PROMPT - } - - # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: - if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { - Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force - } - - # Leave deactivate function in the global namespace if requested: - if (-not $NonDestructive) { - Remove-Item -Path function:deactivate - } -} - -<# -.Description -Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the -given folder, and returns them in a map. - -For each line in the pyvenv.cfg file, if that line can be parsed into exactly -two strings separated by `=` (with any amount of whitespace surrounding the =) -then it is considered a `key = value` line. The left hand string is the key, -the right hand is the value. - -If the value starts with a `'` or a `"` then the first and last character is -stripped from the value before being captured. - -.Parameter ConfigDir -Path to the directory that contains the `pyvenv.cfg` file. -#> -function Get-PyVenvConfig( - [String] - $ConfigDir -) { - Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" - - # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). - $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue - - # An empty map will be returned if no config file is found. - $pyvenvConfig = @{ } - - if ($pyvenvConfigPath) { - - Write-Verbose "File exists, parse `key = value` lines" - $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath - - $pyvenvConfigContent | ForEach-Object { - $keyval = $PSItem -split "\s*=\s*", 2 - if ($keyval[0] -and $keyval[1]) { - $val = $keyval[1] - - # Remove extraneous quotations around a string value. - if ("'""".Contains($val.Substring(0, 1))) { - $val = $val.Substring(1, $val.Length - 2) - } - - $pyvenvConfig[$keyval[0]] = $val - Write-Verbose "Adding Key: '$($keyval[0])'='$val'" - } - } - } - return $pyvenvConfig -} - - -<# Begin Activate script --------------------------------------------------- #> - -# Determine the containing directory of this script -$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition -$VenvExecDir = Get-Item -Path $VenvExecPath - -Write-Verbose "Activation script is located in path: '$VenvExecPath'" -Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" -Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" - -# Set values required in priority: CmdLine, ConfigFile, Default -# First, get the location of the virtual environment, it might not be -# VenvExecDir if specified on the command line. -if ($VenvDir) { - Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" -} -else { - Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." - $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") - Write-Verbose "VenvDir=$VenvDir" -} - -# Next, read the `pyvenv.cfg` file to determine any required value such -# as `prompt`. -$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir - -# Next, set the prompt from the command line, or the config file, or -# just use the name of the virtual environment folder. -if ($Prompt) { - Write-Verbose "Prompt specified as argument, using '$Prompt'" -} -else { - Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" - if ($pyvenvCfg -and $pyvenvCfg['prompt']) { - Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" - $Prompt = $pyvenvCfg['prompt']; - } - else { - Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" - Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" - $Prompt = Split-Path -Path $venvDir -Leaf - } -} - -Write-Verbose "Prompt = '$Prompt'" -Write-Verbose "VenvDir='$VenvDir'" - -# Deactivate any currently active virtual environment, but leave the -# deactivate function in place. -deactivate -nondestructive - -# Now set the environment variable VIRTUAL_ENV, used by many tools to determine -# that there is an activated venv. -$env:VIRTUAL_ENV = $VenvDir - -if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { - - Write-Verbose "Setting prompt to '$Prompt'" - - # Set the prompt to include the env name - # Make sure _OLD_VIRTUAL_PROMPT is global - function global:_OLD_VIRTUAL_PROMPT { "" } - Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT - New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt - - function global:prompt { - Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " - _OLD_VIRTUAL_PROMPT - } - $env:VIRTUAL_ENV_PROMPT = $Prompt -} - -# Clear PYTHONHOME -if (Test-Path -Path Env:PYTHONHOME) { - Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME - Remove-Item -Path Env:PYTHONHOME -} - -# Add the venv to the PATH -Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH -$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/tests/interop/python-redis-typescript/venv/bin/activate b/tests/interop/python-redis-typescript/venv/bin/activate deleted file mode 100644 index 2deb9db..0000000 --- a/tests/interop/python-redis-typescript/venv/bin/activate +++ /dev/null @@ -1,69 +0,0 @@ -# This file must be used with "source bin/activate" *from bash* -# you cannot run it directly - -deactivate () { - # reset old environment variables - if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then - PATH="${_OLD_VIRTUAL_PATH:-}" - export PATH - unset _OLD_VIRTUAL_PATH - fi - if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then - PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" - export PYTHONHOME - unset _OLD_VIRTUAL_PYTHONHOME - fi - - # This should detect bash and zsh, which have a hash command that must - # be called to get it to forget past commands. Without forgetting - # past commands the $PATH changes we made may not be respected - if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then - hash -r 2> /dev/null - fi - - if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then - PS1="${_OLD_VIRTUAL_PS1:-}" - export PS1 - unset _OLD_VIRTUAL_PS1 - fi - - unset VIRTUAL_ENV - unset VIRTUAL_ENV_PROMPT - if [ ! "${1:-}" = "nondestructive" ] ; then - # Self destruct! - unset -f deactivate - fi -} - -# unset irrelevant variables -deactivate nondestructive - -VIRTUAL_ENV="/Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv" -export VIRTUAL_ENV - -_OLD_VIRTUAL_PATH="$PATH" -PATH="$VIRTUAL_ENV/bin:$PATH" -export PATH - -# unset PYTHONHOME if set -# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) -# could use `if (set -u; : $PYTHONHOME) ;` in bash -if [ -n "${PYTHONHOME:-}" ] ; then - _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" - unset PYTHONHOME -fi - -if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then - _OLD_VIRTUAL_PS1="${PS1:-}" - PS1="(venv) ${PS1:-}" - export PS1 - VIRTUAL_ENV_PROMPT="(venv) " - export VIRTUAL_ENV_PROMPT -fi - -# This should detect bash and zsh, which have a hash command that must -# be called to get it to forget past commands. Without forgetting -# past commands the $PATH changes we made may not be respected -if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then - hash -r 2> /dev/null -fi diff --git a/tests/interop/python-redis-typescript/venv/bin/activate.csh b/tests/interop/python-redis-typescript/venv/bin/activate.csh deleted file mode 100644 index 7f6a931..0000000 --- a/tests/interop/python-redis-typescript/venv/bin/activate.csh +++ /dev/null @@ -1,26 +0,0 @@ -# This file must be used with "source bin/activate.csh" *from csh*. -# You cannot run it directly. -# Created by Davide Di Blasi . -# Ported to Python 3.3 venv by Andrew Svetlov - -alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' - -# Unset irrelevant variables. -deactivate nondestructive - -setenv VIRTUAL_ENV "/Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv" - -set _OLD_VIRTUAL_PATH="$PATH" -setenv PATH "$VIRTUAL_ENV/bin:$PATH" - - -set _OLD_VIRTUAL_PROMPT="$prompt" - -if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then - set prompt = "(venv) $prompt" - setenv VIRTUAL_ENV_PROMPT "(venv) " -endif - -alias pydoc python -m pydoc - -rehash diff --git a/tests/interop/python-redis-typescript/venv/bin/activate.fish b/tests/interop/python-redis-typescript/venv/bin/activate.fish deleted file mode 100644 index e86f9aa..0000000 --- a/tests/interop/python-redis-typescript/venv/bin/activate.fish +++ /dev/null @@ -1,69 +0,0 @@ -# This file must be used with "source /bin/activate.fish" *from fish* -# (https://fishshell.com/); you cannot run it directly. - -function deactivate -d "Exit virtual environment and return to normal shell environment" - # reset old environment variables - if test -n "$_OLD_VIRTUAL_PATH" - set -gx PATH $_OLD_VIRTUAL_PATH - set -e _OLD_VIRTUAL_PATH - end - if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME - set -e _OLD_VIRTUAL_PYTHONHOME - end - - if test -n "$_OLD_FISH_PROMPT_OVERRIDE" - set -e _OLD_FISH_PROMPT_OVERRIDE - # prevents error when using nested fish instances (Issue #93858) - if functions -q _old_fish_prompt - functions -e fish_prompt - functions -c _old_fish_prompt fish_prompt - functions -e _old_fish_prompt - end - end - - set -e VIRTUAL_ENV - set -e VIRTUAL_ENV_PROMPT - if test "$argv[1]" != "nondestructive" - # Self-destruct! - functions -e deactivate - end -end - -# Unset irrelevant variables. -deactivate nondestructive - -set -gx VIRTUAL_ENV "/Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv" - -set -gx _OLD_VIRTUAL_PATH $PATH -set -gx PATH "$VIRTUAL_ENV/bin" $PATH - -# Unset PYTHONHOME if set. -if set -q PYTHONHOME - set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME - set -e PYTHONHOME -end - -if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - # fish uses a function instead of an env var to generate the prompt. - - # Save the current fish_prompt function as the function _old_fish_prompt. - functions -c fish_prompt _old_fish_prompt - - # With the original prompt function renamed, we can override with our own. - function fish_prompt - # Save the return status of the last command. - set -l old_status $status - - # Output the venv prompt; color taken from the blue of the Python logo. - printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal) - - # Restore the return status of the previous command. - echo "exit $old_status" | . - # Output the original/"old" prompt. - _old_fish_prompt - end - - set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" - set -gx VIRTUAL_ENV_PROMPT "(venv) " -end diff --git a/tests/interop/python-redis-typescript/venv/bin/pip b/tests/interop/python-redis-typescript/venv/bin/pip deleted file mode 100755 index bb64f9f..0000000 --- a/tests/interop/python-redis-typescript/venv/bin/pip +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv/bin/python3.11 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/tests/interop/python-redis-typescript/venv/bin/pip3 b/tests/interop/python-redis-typescript/venv/bin/pip3 deleted file mode 100755 index bb64f9f..0000000 --- a/tests/interop/python-redis-typescript/venv/bin/pip3 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv/bin/python3.11 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/tests/interop/python-redis-typescript/venv/bin/pip3.11 b/tests/interop/python-redis-typescript/venv/bin/pip3.11 deleted file mode 100755 index bb64f9f..0000000 --- a/tests/interop/python-redis-typescript/venv/bin/pip3.11 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv/bin/python3.11 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/tests/interop/python-redis-typescript/venv/bin/python b/tests/interop/python-redis-typescript/venv/bin/python deleted file mode 120000 index 6e7f3c7..0000000 --- a/tests/interop/python-redis-typescript/venv/bin/python +++ /dev/null @@ -1 +0,0 @@ -python3.11 \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/venv/bin/python3 b/tests/interop/python-redis-typescript/venv/bin/python3 deleted file mode 120000 index 6e7f3c7..0000000 --- a/tests/interop/python-redis-typescript/venv/bin/python3 +++ /dev/null @@ -1 +0,0 @@ -python3.11 \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/venv/bin/python3.11 b/tests/interop/python-redis-typescript/venv/bin/python3.11 deleted file mode 120000 index 2d17577..0000000 --- a/tests/interop/python-redis-typescript/venv/bin/python3.11 +++ /dev/null @@ -1 +0,0 @@ -/Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11 \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/venv/pyvenv.cfg b/tests/interop/python-redis-typescript/venv/pyvenv.cfg deleted file mode 100644 index e5a0403..0000000 --- a/tests/interop/python-redis-typescript/venv/pyvenv.cfg +++ /dev/null @@ -1,5 +0,0 @@ -home = /Library/Frameworks/Python.framework/Versions/3.11/bin -include-system-site-packages = false -version = 3.11.6 -executable = /Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11 -command = /usr/local/bin/python3 -m venv /Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv diff --git a/tests/interop/typescript-redis-python/venv/.gitignore b/tests/interop/typescript-redis-python/venv/.gitignore deleted file mode 100644 index 61d97f0..0000000 --- a/tests/interop/typescript-redis-python/venv/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/venv/bin/Activate.ps1 b/tests/interop/typescript-redis-python/venv/bin/Activate.ps1 deleted file mode 100644 index b49d77b..0000000 --- a/tests/interop/typescript-redis-python/venv/bin/Activate.ps1 +++ /dev/null @@ -1,247 +0,0 @@ -<# -.Synopsis -Activate a Python virtual environment for the current PowerShell session. - -.Description -Pushes the python executable for a virtual environment to the front of the -$Env:PATH environment variable and sets the prompt to signify that you are -in a Python virtual environment. Makes use of the command line switches as -well as the `pyvenv.cfg` file values present in the virtual environment. - -.Parameter VenvDir -Path to the directory that contains the virtual environment to activate. The -default value for this is the parent of the directory that the Activate.ps1 -script is located within. - -.Parameter Prompt -The prompt prefix to display when this virtual environment is activated. By -default, this prompt is the name of the virtual environment folder (VenvDir) -surrounded by parentheses and followed by a single space (ie. '(.venv) '). - -.Example -Activate.ps1 -Activates the Python virtual environment that contains the Activate.ps1 script. - -.Example -Activate.ps1 -Verbose -Activates the Python virtual environment that contains the Activate.ps1 script, -and shows extra information about the activation as it executes. - -.Example -Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv -Activates the Python virtual environment located in the specified location. - -.Example -Activate.ps1 -Prompt "MyPython" -Activates the Python virtual environment that contains the Activate.ps1 script, -and prefixes the current prompt with the specified string (surrounded in -parentheses) while the virtual environment is active. - -.Notes -On Windows, it may be required to enable this Activate.ps1 script by setting the -execution policy for the user. You can do this by issuing the following PowerShell -command: - -PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - -For more information on Execution Policies: -https://go.microsoft.com/fwlink/?LinkID=135170 - -#> -Param( - [Parameter(Mandatory = $false)] - [String] - $VenvDir, - [Parameter(Mandatory = $false)] - [String] - $Prompt -) - -<# Function declarations --------------------------------------------------- #> - -<# -.Synopsis -Remove all shell session elements added by the Activate script, including the -addition of the virtual environment's Python executable from the beginning of -the PATH variable. - -.Parameter NonDestructive -If present, do not remove this function from the global namespace for the -session. - -#> -function global:deactivate ([switch]$NonDestructive) { - # Revert to original values - - # The prior prompt: - if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { - Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt - Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT - } - - # The prior PYTHONHOME: - if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { - Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME - Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME - } - - # The prior PATH: - if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { - Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH - Remove-Item -Path Env:_OLD_VIRTUAL_PATH - } - - # Just remove the VIRTUAL_ENV altogether: - if (Test-Path -Path Env:VIRTUAL_ENV) { - Remove-Item -Path env:VIRTUAL_ENV - } - - # Just remove VIRTUAL_ENV_PROMPT altogether. - if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { - Remove-Item -Path env:VIRTUAL_ENV_PROMPT - } - - # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: - if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { - Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force - } - - # Leave deactivate function in the global namespace if requested: - if (-not $NonDestructive) { - Remove-Item -Path function:deactivate - } -} - -<# -.Description -Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the -given folder, and returns them in a map. - -For each line in the pyvenv.cfg file, if that line can be parsed into exactly -two strings separated by `=` (with any amount of whitespace surrounding the =) -then it is considered a `key = value` line. The left hand string is the key, -the right hand is the value. - -If the value starts with a `'` or a `"` then the first and last character is -stripped from the value before being captured. - -.Parameter ConfigDir -Path to the directory that contains the `pyvenv.cfg` file. -#> -function Get-PyVenvConfig( - [String] - $ConfigDir -) { - Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" - - # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). - $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue - - # An empty map will be returned if no config file is found. - $pyvenvConfig = @{ } - - if ($pyvenvConfigPath) { - - Write-Verbose "File exists, parse `key = value` lines" - $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath - - $pyvenvConfigContent | ForEach-Object { - $keyval = $PSItem -split "\s*=\s*", 2 - if ($keyval[0] -and $keyval[1]) { - $val = $keyval[1] - - # Remove extraneous quotations around a string value. - if ("'""".Contains($val.Substring(0, 1))) { - $val = $val.Substring(1, $val.Length - 2) - } - - $pyvenvConfig[$keyval[0]] = $val - Write-Verbose "Adding Key: '$($keyval[0])'='$val'" - } - } - } - return $pyvenvConfig -} - - -<# Begin Activate script --------------------------------------------------- #> - -# Determine the containing directory of this script -$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition -$VenvExecDir = Get-Item -Path $VenvExecPath - -Write-Verbose "Activation script is located in path: '$VenvExecPath'" -Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" -Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" - -# Set values required in priority: CmdLine, ConfigFile, Default -# First, get the location of the virtual environment, it might not be -# VenvExecDir if specified on the command line. -if ($VenvDir) { - Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" -} -else { - Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." - $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") - Write-Verbose "VenvDir=$VenvDir" -} - -# Next, read the `pyvenv.cfg` file to determine any required value such -# as `prompt`. -$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir - -# Next, set the prompt from the command line, or the config file, or -# just use the name of the virtual environment folder. -if ($Prompt) { - Write-Verbose "Prompt specified as argument, using '$Prompt'" -} -else { - Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" - if ($pyvenvCfg -and $pyvenvCfg['prompt']) { - Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" - $Prompt = $pyvenvCfg['prompt']; - } - else { - Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" - Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" - $Prompt = Split-Path -Path $venvDir -Leaf - } -} - -Write-Verbose "Prompt = '$Prompt'" -Write-Verbose "VenvDir='$VenvDir'" - -# Deactivate any currently active virtual environment, but leave the -# deactivate function in place. -deactivate -nondestructive - -# Now set the environment variable VIRTUAL_ENV, used by many tools to determine -# that there is an activated venv. -$env:VIRTUAL_ENV = $VenvDir - -if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { - - Write-Verbose "Setting prompt to '$Prompt'" - - # Set the prompt to include the env name - # Make sure _OLD_VIRTUAL_PROMPT is global - function global:_OLD_VIRTUAL_PROMPT { "" } - Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT - New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt - - function global:prompt { - Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " - _OLD_VIRTUAL_PROMPT - } - $env:VIRTUAL_ENV_PROMPT = $Prompt -} - -# Clear PYTHONHOME -if (Test-Path -Path Env:PYTHONHOME) { - Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME - Remove-Item -Path Env:PYTHONHOME -} - -# Add the venv to the PATH -Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH -$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/tests/interop/typescript-redis-python/venv/bin/activate b/tests/interop/typescript-redis-python/venv/bin/activate deleted file mode 100644 index d2de552..0000000 --- a/tests/interop/typescript-redis-python/venv/bin/activate +++ /dev/null @@ -1,69 +0,0 @@ -# This file must be used with "source bin/activate" *from bash* -# you cannot run it directly - -deactivate () { - # reset old environment variables - if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then - PATH="${_OLD_VIRTUAL_PATH:-}" - export PATH - unset _OLD_VIRTUAL_PATH - fi - if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then - PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" - export PYTHONHOME - unset _OLD_VIRTUAL_PYTHONHOME - fi - - # This should detect bash and zsh, which have a hash command that must - # be called to get it to forget past commands. Without forgetting - # past commands the $PATH changes we made may not be respected - if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then - hash -r 2> /dev/null - fi - - if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then - PS1="${_OLD_VIRTUAL_PS1:-}" - export PS1 - unset _OLD_VIRTUAL_PS1 - fi - - unset VIRTUAL_ENV - unset VIRTUAL_ENV_PROMPT - if [ ! "${1:-}" = "nondestructive" ] ; then - # Self destruct! - unset -f deactivate - fi -} - -# unset irrelevant variables -deactivate nondestructive - -VIRTUAL_ENV="/Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv" -export VIRTUAL_ENV - -_OLD_VIRTUAL_PATH="$PATH" -PATH="$VIRTUAL_ENV/bin:$PATH" -export PATH - -# unset PYTHONHOME if set -# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) -# could use `if (set -u; : $PYTHONHOME) ;` in bash -if [ -n "${PYTHONHOME:-}" ] ; then - _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" - unset PYTHONHOME -fi - -if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then - _OLD_VIRTUAL_PS1="${PS1:-}" - PS1="(venv) ${PS1:-}" - export PS1 - VIRTUAL_ENV_PROMPT="(venv) " - export VIRTUAL_ENV_PROMPT -fi - -# This should detect bash and zsh, which have a hash command that must -# be called to get it to forget past commands. Without forgetting -# past commands the $PATH changes we made may not be respected -if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then - hash -r 2> /dev/null -fi diff --git a/tests/interop/typescript-redis-python/venv/bin/activate.csh b/tests/interop/typescript-redis-python/venv/bin/activate.csh deleted file mode 100644 index 1d98448..0000000 --- a/tests/interop/typescript-redis-python/venv/bin/activate.csh +++ /dev/null @@ -1,26 +0,0 @@ -# This file must be used with "source bin/activate.csh" *from csh*. -# You cannot run it directly. -# Created by Davide Di Blasi . -# Ported to Python 3.3 venv by Andrew Svetlov - -alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' - -# Unset irrelevant variables. -deactivate nondestructive - -setenv VIRTUAL_ENV "/Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv" - -set _OLD_VIRTUAL_PATH="$PATH" -setenv PATH "$VIRTUAL_ENV/bin:$PATH" - - -set _OLD_VIRTUAL_PROMPT="$prompt" - -if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then - set prompt = "(venv) $prompt" - setenv VIRTUAL_ENV_PROMPT "(venv) " -endif - -alias pydoc python -m pydoc - -rehash diff --git a/tests/interop/typescript-redis-python/venv/bin/activate.fish b/tests/interop/typescript-redis-python/venv/bin/activate.fish deleted file mode 100644 index a9f566a..0000000 --- a/tests/interop/typescript-redis-python/venv/bin/activate.fish +++ /dev/null @@ -1,69 +0,0 @@ -# This file must be used with "source /bin/activate.fish" *from fish* -# (https://fishshell.com/); you cannot run it directly. - -function deactivate -d "Exit virtual environment and return to normal shell environment" - # reset old environment variables - if test -n "$_OLD_VIRTUAL_PATH" - set -gx PATH $_OLD_VIRTUAL_PATH - set -e _OLD_VIRTUAL_PATH - end - if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME - set -e _OLD_VIRTUAL_PYTHONHOME - end - - if test -n "$_OLD_FISH_PROMPT_OVERRIDE" - set -e _OLD_FISH_PROMPT_OVERRIDE - # prevents error when using nested fish instances (Issue #93858) - if functions -q _old_fish_prompt - functions -e fish_prompt - functions -c _old_fish_prompt fish_prompt - functions -e _old_fish_prompt - end - end - - set -e VIRTUAL_ENV - set -e VIRTUAL_ENV_PROMPT - if test "$argv[1]" != "nondestructive" - # Self-destruct! - functions -e deactivate - end -end - -# Unset irrelevant variables. -deactivate nondestructive - -set -gx VIRTUAL_ENV "/Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv" - -set -gx _OLD_VIRTUAL_PATH $PATH -set -gx PATH "$VIRTUAL_ENV/bin" $PATH - -# Unset PYTHONHOME if set. -if set -q PYTHONHOME - set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME - set -e PYTHONHOME -end - -if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - # fish uses a function instead of an env var to generate the prompt. - - # Save the current fish_prompt function as the function _old_fish_prompt. - functions -c fish_prompt _old_fish_prompt - - # With the original prompt function renamed, we can override with our own. - function fish_prompt - # Save the return status of the last command. - set -l old_status $status - - # Output the venv prompt; color taken from the blue of the Python logo. - printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal) - - # Restore the return status of the previous command. - echo "exit $old_status" | . - # Output the original/"old" prompt. - _old_fish_prompt - end - - set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" - set -gx VIRTUAL_ENV_PROMPT "(venv) " -end diff --git a/tests/interop/typescript-redis-python/venv/bin/pip b/tests/interop/typescript-redis-python/venv/bin/pip deleted file mode 100755 index 10ff886..0000000 --- a/tests/interop/typescript-redis-python/venv/bin/pip +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv/bin/python3.11 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/tests/interop/typescript-redis-python/venv/bin/pip3 b/tests/interop/typescript-redis-python/venv/bin/pip3 deleted file mode 100755 index 10ff886..0000000 --- a/tests/interop/typescript-redis-python/venv/bin/pip3 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv/bin/python3.11 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/tests/interop/typescript-redis-python/venv/bin/pip3.11 b/tests/interop/typescript-redis-python/venv/bin/pip3.11 deleted file mode 100755 index 10ff886..0000000 --- a/tests/interop/typescript-redis-python/venv/bin/pip3.11 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv/bin/python3.11 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/tests/interop/typescript-redis-python/venv/bin/python b/tests/interop/typescript-redis-python/venv/bin/python deleted file mode 120000 index 6e7f3c7..0000000 --- a/tests/interop/typescript-redis-python/venv/bin/python +++ /dev/null @@ -1 +0,0 @@ -python3.11 \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/venv/bin/python3 b/tests/interop/typescript-redis-python/venv/bin/python3 deleted file mode 120000 index 6e7f3c7..0000000 --- a/tests/interop/typescript-redis-python/venv/bin/python3 +++ /dev/null @@ -1 +0,0 @@ -python3.11 \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/venv/bin/python3.11 b/tests/interop/typescript-redis-python/venv/bin/python3.11 deleted file mode 120000 index 2d17577..0000000 --- a/tests/interop/typescript-redis-python/venv/bin/python3.11 +++ /dev/null @@ -1 +0,0 @@ -/Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11 \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/venv/pyvenv.cfg b/tests/interop/typescript-redis-python/venv/pyvenv.cfg deleted file mode 100644 index 7f80dd2..0000000 --- a/tests/interop/typescript-redis-python/venv/pyvenv.cfg +++ /dev/null @@ -1,5 +0,0 @@ -home = /Library/Frameworks/Python.framework/Versions/3.11/bin -include-system-site-packages = false -version = 3.11.6 -executable = /Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11 -command = /Users/boateng/Documents/work/browserstate/tests/interop/python-redis-typescript/venv/bin/python3 -m venv /Users/boateng/Documents/work/browserstate/tests/interop/typescript-redis-python/venv From 9dff345d02b0e2b4a5b83866da007f46cd3b5c10 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 04:40:06 +0100 Subject: [PATCH 13/20] Chore: Added ioredis dependency to TypeScript-Redis-Python tests --- .../typescript-redis-python/package-lock.json | 115 +++++++++++++++++- .../typescript-redis-python/package.json | 5 +- 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/tests/interop/typescript-redis-python/package-lock.json b/tests/interop/typescript-redis-python/package-lock.json index ff7af30..6fd3fa5 100644 --- a/tests/interop/typescript-redis-python/package-lock.json +++ b/tests/interop/typescript-redis-python/package-lock.json @@ -7,7 +7,120 @@ "": { "name": "typescript-redis-python", "version": "1.0.0", - "license": "ISC" + "license": "ISC", + "dependencies": { + "ioredis": "^5.6.0" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/ioredis": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz", + "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" } } } diff --git a/tests/interop/typescript-redis-python/package.json b/tests/interop/typescript-redis-python/package.json index cc3970a..12944b0 100644 --- a/tests/interop/typescript-redis-python/package.json +++ b/tests/interop/typescript-redis-python/package.json @@ -9,5 +9,8 @@ "keywords": [], "author": "", "license": "ISC", - "description": "This test verifies the cross-language interoperability between TypeScript and Python implementations of BrowserState using Redis as the storage backend." + "description": "This test verifies the cross-language interoperability between TypeScript and Python implementations of BrowserState using Redis as the storage backend.", + "dependencies": { + "ioredis": "^5.6.0" + } } From 446d9db68ab7fb3b77cf150327e4f0f27d0155f1 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 04:47:19 +0100 Subject: [PATCH 14/20] Chore: Removed debug and test import files that are not needed --- .../python-redis-typescript/debug_verify.mjs | 56 ------------------- .../python-redis-typescript/test_import.mjs | 54 ------------------ 2 files changed, 110 deletions(-) delete mode 100644 tests/interop/python-redis-typescript/debug_verify.mjs delete mode 100644 tests/interop/python-redis-typescript/test_import.mjs diff --git a/tests/interop/python-redis-typescript/debug_verify.mjs b/tests/interop/python-redis-typescript/debug_verify.mjs deleted file mode 100644 index 5fc69f8..0000000 --- a/tests/interop/python-redis-typescript/debug_verify.mjs +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Debug script for the verification process - */ - -import { fileURLToPath } from 'url'; -import path from 'path'; -import fs from 'fs'; - -// Get current directory -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -console.log('🔍 Starting debug verification process...'); - -// Try importing modules -console.log('\nImporting modules...'); -try { - console.log('Importing BrowserState...'); - const { BrowserState } = await import('../../../typescript/dist/index.js'); - console.log('✅ Successfully imported BrowserState!'); - - console.log('\nImporting playwright...'); - const { chromium } = await import('playwright'); - console.log('✅ Successfully imported playwright!'); - - // Try to create a BrowserState instance - console.log('\nTrying to instantiate BrowserState with Redis...'); - const browserState = new BrowserState({ - userId: 'interop_test_user', - storageType: 'redis', - redisOptions: { - host: 'localhost', - port: 6379, - password: undefined, - db: 0, - keyPrefix: 'browserstate:', - ttl: 604800 // 7 days - } - }); - console.log('✅ Successfully created BrowserState instance!'); - - // Try to execute Redis-specific methods to see if they fail - console.log('\nList available sessions (this might fail if Redis connection issues)...'); - try { - const sessions = await browserState.listSessions(); - console.log(`✅ Found ${sessions.length} sessions: ${sessions.join(', ')}`); - } catch (err) { - console.error(`❌ Error listing sessions: ${err.message}`); - console.error('This might be due to Redis connectivity issues'); - } -} catch (err) { - console.error('❌ Error in debug process:'); - console.error(err); -} - -console.log('\n✨ Debug process complete!'); \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/test_import.mjs b/tests/interop/python-redis-typescript/test_import.mjs deleted file mode 100644 index 13f0c7a..0000000 --- a/tests/interop/python-redis-typescript/test_import.mjs +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Simple test script to verify we can import BrowserState from the TypeScript package. - */ - -import { fileURLToPath } from 'url'; -import path from 'path'; -import fs from 'fs'; - -// Get current directory -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -console.log('🔍 Checking TypeScript package output...'); - -// Check if the dist directory exists -const distPath = path.join(__dirname, '../../../typescript/dist'); -console.log(`Checking if dist directory exists at: ${distPath}`); -console.log(`Dist directory exists: ${fs.existsSync(distPath)}`); - -// Check if the index.js file exists -const indexPath = path.join(distPath, 'index.js'); -console.log(`Checking if index.js exists at: ${indexPath}`); -console.log(`index.js exists: ${fs.existsSync(indexPath)}`); - -// List files in the dist directory -console.log('\nListing files in dist directory:'); -try { - const files = fs.readdirSync(distPath); - files.forEach(file => console.log(`- ${file}`)); -} catch (err) { - console.error(`Error listing files: ${err.message}`); -} - -console.log('\nAttempting to import BrowserState...'); -try { - const { BrowserState } = await import('../../../typescript/dist/index.js'); - console.log('✅ Successfully imported BrowserState!'); - console.log(`BrowserState type: ${typeof BrowserState}`); - - // Try to create a BrowserState instance - if (typeof BrowserState === 'function') { - console.log('\nTrying to instantiate BrowserState...'); - const browserState = new BrowserState({ - userId: 'test-user' - }); - console.log('✅ Successfully created BrowserState instance!'); - console.log(`Instance type: ${typeof browserState}`); - } -} catch (err) { - console.error('❌ Error importing BrowserState:'); - console.error(err); -} - -console.log('\n✨ Import test complete!'); \ No newline at end of file From 1305b88913c5082ed07c6d6a80583e2d85e537f4 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 1 Apr 2025 05:23:21 +0100 Subject: [PATCH 15/20] Fix: Improved Redis key format validation for interoperability --- python/browserstate/storage/redis_storage.py | 49 ++++++++----------- .../test_cross_language.py | 2 +- .../python-redis-typescript/verify_state.mjs | 2 +- .../typescript-redis-python/create_state.mjs | 2 +- .../typescript-redis-python/verify_state.py | 2 +- typescript/src/storage/RedisStorage.ts | 10 +++- 6 files changed, 32 insertions(+), 35 deletions(-) diff --git a/python/browserstate/storage/redis_storage.py b/python/browserstate/storage/redis_storage.py index 7e40bfb..ebbe28e 100644 --- a/python/browserstate/storage/redis_storage.py +++ b/python/browserstate/storage/redis_storage.py @@ -27,39 +27,26 @@ def __init__(self, Args: redis_url: Redis connection URL. - key_prefix: Prefix to use for keys in Redis. + key_prefix: Prefix to use for keys in Redis. Must not contain colons. """ + # Validate key_prefix format + if ":" in key_prefix: + raise ValueError("key_prefix must not contain colons (:). The implementation automatically builds Redis keys in the format: {prefix}{userId}:{sessionId}") + self.redis_client = redis.Redis.from_url(redis_url) - - # Remove trailing colon if it exists to prevent double colons - self.key_prefix = key_prefix.rstrip(':') + self.key_prefix = key_prefix logging.info(f"Redis storage initialized with prefix: {self.key_prefix}") def _get_key(self, user_id: str, session_id: str) -> str: - """ - Generate a Redis key for a given user and session. - """ - # Create key with format "prefix:userId:sessionId" (ensure no double colons) - key = f"{self.key_prefix}:{user_id}:{session_id}" - return key + """Generate a Redis key for a given user and session.""" + return f"{self.key_prefix}{user_id}:{session_id}" def _get_metadata_key(self, user_id: str, session_id: str) -> str: - """ - Generate a Redis key for session metadata. - """ - return f"{self.key_prefix}:{user_id}:{session_id}:metadata" + """Generate a Redis key for session metadata.""" + return f"{self.key_prefix}{user_id}:{session_id}:metadata" def _get_temp_path(self, user_id: str, session_id: str) -> str: - """ - Get a temporary path for a session. - - Args: - user_id: User identifier. - session_id: Session identifier. - - Returns: - Full path to the temporary session directory. - """ + """Get a temporary path for a session.""" temp_dir = os.path.join(tempfile.gettempdir(), "browserstate", user_id) os.makedirs(temp_dir, exist_ok=True) return os.path.join(temp_dir, session_id) @@ -179,7 +166,7 @@ def upload(self, user_id: str, session_id: str, file_path: str) -> None: # Create metadata (matching TypeScript metadata format) metadata = { "timestamp": time.time() * 1000, # Current time in milliseconds - "version": "2.0", # Match TypeScript version + "version": "2.0", } # Store metadata in Redis @@ -209,7 +196,7 @@ def list_sessions(self, user_id: str) -> List[str]: Returns: List of session identifiers. """ - pattern = f"{self.key_prefix}:{user_id}:*" + pattern = f"{self.key_prefix}{user_id}:*" logging.info(f"Listing sessions with pattern: {pattern}") try: @@ -217,9 +204,13 @@ def list_sessions(self, user_id: str) -> List[str]: session_ids = [] for key in keys: key_str = key.decode('utf-8') if isinstance(key, bytes) else key - parts = key_str.split(':') - if len(parts) == 3: - session_ids.append(parts[2]) + # Extract sessionId from key + parts = key_str[len(self.key_prefix) + len(user_id) + 1:].split(':') + session_id = parts[0] + + # Exclude metadata keys and deduplicate + if len(parts) == 1 and session_id not in session_ids: + session_ids.append(session_id) logging.info(f"Found {len(session_ids)} sessions for user {user_id}") return session_ids diff --git a/tests/interop/python-redis-typescript/test_cross_language.py b/tests/interop/python-redis-typescript/test_cross_language.py index 93ea3be..d90e150 100644 --- a/tests/interop/python-redis-typescript/test_cross_language.py +++ b/tests/interop/python-redis-typescript/test_cross_language.py @@ -14,7 +14,7 @@ # Redis configuration for Python REDIS_URL = "redis://localhost:6379/0" -REDIS_KEY_PREFIX = "browserstate:" +REDIS_KEY_PREFIX = "browserstate" # Test session ID - must be the same across Python and TypeScript SESSION_ID = "cross_language_test" diff --git a/tests/interop/python-redis-typescript/verify_state.mjs b/tests/interop/python-redis-typescript/verify_state.mjs index 3024ae6..e0aceed 100644 --- a/tests/interop/python-redis-typescript/verify_state.mjs +++ b/tests/interop/python-redis-typescript/verify_state.mjs @@ -26,7 +26,7 @@ const REDIS_CONFIG = { port: 6379, password: undefined, db: 0, - keyPrefix: 'browserstate:', + keyPrefix: 'browserstate', ttl: 604800 // 7 days }; diff --git a/tests/interop/typescript-redis-python/create_state.mjs b/tests/interop/typescript-redis-python/create_state.mjs index dbb2672..4abc78f 100644 --- a/tests/interop/typescript-redis-python/create_state.mjs +++ b/tests/interop/typescript-redis-python/create_state.mjs @@ -25,7 +25,7 @@ const REDIS_CONFIG = { port: 6379, password: undefined, db: 0, - keyPrefix: 'browserstate:', + keyPrefix: 'browserstate', ttl: 604800 // 7 days }; diff --git a/tests/interop/typescript-redis-python/verify_state.py b/tests/interop/typescript-redis-python/verify_state.py index f1ea415..a327ab7 100644 --- a/tests/interop/typescript-redis-python/verify_state.py +++ b/tests/interop/typescript-redis-python/verify_state.py @@ -14,7 +14,7 @@ # Redis configuration for Python REDIS_URL = "redis://localhost:6379/0" -REDIS_KEY_PREFIX = "browserstate:" +REDIS_KEY_PREFIX = "browserstate" # Test constants - must match TypeScript test SESSION_ID = "typescript_to_python_test" diff --git a/typescript/src/storage/RedisStorage.ts b/typescript/src/storage/RedisStorage.ts index faa98f7..4ac46ba 100644 --- a/typescript/src/storage/RedisStorage.ts +++ b/typescript/src/storage/RedisStorage.ts @@ -120,7 +120,7 @@ export interface RedisStorageOptions { /** * Prefix for Redis keys to avoid collisions with other applications - * @default "browserstate:" + * @default "browserstate" */ keyPrefix?: string; @@ -173,7 +173,13 @@ export class RedisStorageProvider implements StorageProvider { * @param options - Redis connection and storage configuration */ constructor(options: RedisStorageOptions) { - this.keyPrefix = options.keyPrefix || "browserstate:"; + this.keyPrefix = options.keyPrefix || "browserstate"; + + // Validate keyPrefix format + if (this.keyPrefix.includes(':')) { + throw new Error("keyPrefix must not contain colons (:). The implementation automatically builds Redis keys in the format: {prefix}{userId}:{sessionId}"); + } + this.tempDir = options.tempDir || os.tmpdir(); this.ttl = options.ttl; this.options = options; From 3999a4596f0c0c864da09af44c985aa154defe23 Mon Sep 17 00:00:00 2001 From: Sagar Date: Tue, 1 Apr 2025 15:40:42 -0400 Subject: [PATCH 16/20] Added chrome-redis-safari test, chrome and safari localStorage is different, implemented manual extraction for compatibility --- tests/interop/README.md | 24 +- tests/interop/chrome-redis-safari/README.md | 73 +++++ .../chrome-redis-safari/create_state.mjs | 163 +++++++++++ .../chrome-redis-safari/package-lock.json | 94 +++++++ .../interop/chrome-redis-safari/package.json | 15 + .../interop/chrome-redis-safari/run_tests.sh | 91 ++++++ .../interop/chrome-redis-safari/tsconfig.json | 11 + .../chrome-redis-safari/verify_state.mjs | 260 ++++++++++++++++++ .../python-redis-typescript/package.json | 3 +- tests/interop/run_all.sh | 4 + .../typescript-redis-python/package-lock.json | 3 +- .../typescript-redis-python/package.json | 3 +- typescript/package-lock.json | 4 +- 13 files changed, 736 insertions(+), 12 deletions(-) create mode 100644 tests/interop/chrome-redis-safari/README.md create mode 100644 tests/interop/chrome-redis-safari/create_state.mjs create mode 100644 tests/interop/chrome-redis-safari/package-lock.json create mode 100644 tests/interop/chrome-redis-safari/package.json create mode 100755 tests/interop/chrome-redis-safari/run_tests.sh create mode 100644 tests/interop/chrome-redis-safari/tsconfig.json create mode 100644 tests/interop/chrome-redis-safari/verify_state.mjs diff --git a/tests/interop/README.md b/tests/interop/README.md index b98c6d4..f8ae1fb 100644 --- a/tests/interop/README.md +++ b/tests/interop/README.md @@ -1,15 +1,18 @@ # BrowserState Interop Tests -This directory contains interoperability tests between Python and TypeScript implementations of BrowserState using Redis as the storage backend. +This directory contains interoperability tests for BrowserState: +1. Cross-language tests between Python and TypeScript implementations using Redis as the storage backend. +2. Cross-browser tests between different browsers (Chrome and Safari) using Redis as the storage backend. ## Directory Structure ``` tests/interop/ -├── python-redis-typescript/ # Python -> Redis -> TypeScript tests -├── typescript-redis-python/ # TypeScript -> Redis -> Python tests -├── setup.sh # Setup script for all interop tests -└── run_all.sh # Run all interop tests +├── python-redis-typescript/ # Python -> Redis -> TypeScript tests +├── typescript-redis-python/ # TypeScript -> Redis -> Python tests +├── chrome-redis-safari/ # Chrome -> Redis -> Safari tests +├── setup.sh # Setup script for all interop tests +└── run_all.sh # Run all interop tests ``` ## Prerequisites @@ -18,6 +21,7 @@ tests/interop/ 2. Python 3.7+ 3. Node.js with npm 4. TypeScript and ts-node +5. Playwright for browser automation ## Setup @@ -52,8 +56,7 @@ This will: ## Test Structure Each test directory contains: -- A Python script that creates/verifies browser state data -- A TypeScript script that creates/verifies browser state data +- Scripts that create/verify browser state data - A shell script (`run_tests.sh`) that orchestrates the test execution ## Cross-Language Interoperability @@ -65,6 +68,13 @@ These tests verify that: The tests focus on localStorage data persistence through Redis storage. +## Cross-Browser Interoperability + +These tests verify that: + +1. Browser state created in one browser can be correctly accessed in another browser +2. Currently testing Chrome to Safari interoperability through Redis storage + ## GitHub Workflow Integration These tests are designed to be run in GitHub workflows. Example workflow step: diff --git a/tests/interop/chrome-redis-safari/README.md b/tests/interop/chrome-redis-safari/README.md new file mode 100644 index 0000000..ee7b95b --- /dev/null +++ b/tests/interop/chrome-redis-safari/README.md @@ -0,0 +1,73 @@ +# Chrome to Safari Browser Interoperability Test + +This directory contains interoperability tests between Chrome and Safari browsers using Redis as the storage backend. + +## Overview + +This test verifies that browser state (specifically localStorage data) can be: +1. Created in Chrome +2. Stored in Redis using the BrowserState library +3. Successfully restored in Safari + +## Prerequisites + +1. Redis server running locally (default port 6379) +2. Node.js with npm +3. Playwright + +## Test Structure + +The test consists of two main scripts: + +1. `create_state.mjs` - Creates browser state in Chrome and stores it in Redis +2. `verify_state.mjs` - Loads the Chrome-created state in Safari and verifies it + +## Running the Test + +To run the entire test suite: + +```bash +./run_tests.sh +``` + +To run individual steps: + +```bash +# Create state in Chrome +node create_state.mjs + +# Verify state in Safari +node verify_state.mjs +``` + +## Test Details + +### Chrome State Creation (`create_state.mjs`) + +1. Initializes a BrowserState instance with Redis storage +2. Mounts a new session for the browser state +3. Launches a Chrome browser with the mounted state +4. Adds test data to localStorage: + - Multiple note items + - Browser metadata (user agent, timestamp) +5. Unmounts the session, saving state to Redis + +### Safari Verification (`verify_state.mjs`) + +1. Initializes a BrowserState instance with the same Redis configuration +2. Lists available sessions to find the Chrome-created session +3. Mounts the Chrome-created session +4. Launches a Safari browser with the mounted state +5. Verifies that the localStorage data created in Chrome is accessible in Safari: + - Notes are present and contain the expected content + - Browser metadata shows it was created in Chrome +6. Adds Safari verification metadata to localStorage +7. Unmounts the session + +## Troubleshooting + +If the test fails, check: + +1. Redis is running: `redis-cli ping` +2. Playwright browsers are installed: `npx playwright install chromium webkit` +3. The TypeScript package is built: `npm --prefix ../../../typescript run build` \ No newline at end of file diff --git a/tests/interop/chrome-redis-safari/create_state.mjs b/tests/interop/chrome-redis-safari/create_state.mjs new file mode 100644 index 0000000..cbbb3e8 --- /dev/null +++ b/tests/interop/chrome-redis-safari/create_state.mjs @@ -0,0 +1,163 @@ +/** + * Script to create browser state in Chrome and store in Redis + * for cross-browser interoperability testing with Safari. + */ + +import path from 'path'; +import { fileURLToPath } from 'url'; +import { BrowserState } from '../../../typescript/dist/index.js'; +import { chromium } from 'playwright'; + +// Get current file directory with ES modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Test constants +const SESSION_ID = 'chrome_to_safari_test'; +const USER_ID = 'browser_interop_user'; + +// Debug mode - set to true to see browser UI +const DEBUG = false; + +// Redis configuration +const REDIS_CONFIG = { + host: 'localhost', + port: 6379, + password: undefined, + db: 0, + keyPrefix: 'browserstate', + ttl: 604800 // 7 days +}; + +// Path to the test HTML file +const TEST_HTML_PATH = path.resolve(path.join(__dirname, '../../../typescript/examples/shared/test.html')); +const TEST_URL = `file://${TEST_HTML_PATH}`; + +// Function to fail the test with a clear error message +function failTest(message) { + console.error(`\n❌ TEST FAILED: ${message}`); + process.exit(1); +} + +async function createTestData() { + console.log('🚀 Starting Chrome State Creation\n'); + + try { + // Initialize BrowserState with Redis storage + console.log('🔧 Creating BrowserState with Redis storage...'); + const browserState = new BrowserState({ + userId: USER_ID, + storageType: 'redis', + redisOptions: REDIS_CONFIG + }); + + // List existing sessions + console.log('\n📋 Listing existing sessions...'); + const sessions = await browserState.listSessions(); + console.log(`Found ${sessions.length} session(s): ${sessions.join(', ')}`); + + // Mount the session + console.log(`\n📥 Mounting session: ${SESSION_ID}`); + const userDataDir = await browserState.mount(SESSION_ID); + console.log(`📂 Mounted at: ${userDataDir}`); + + // Launch Chrome browser with the mounted state + console.log('\n🌐 Launching Chrome browser with Playwright...'); + const browser = await chromium.launchPersistentContext(userDataDir, { + headless: !DEBUG + }); + + try { + // Create a new page + const page = await browser.newPage(); + + // Navigate to the test HTML page + console.log(`\n📄 Loading test page: ${TEST_URL}`); + await page.goto(TEST_URL); + console.log('✅ Test page loaded'); + + // Clear existing localStorage to start fresh + await page.evaluate(() => { + localStorage.clear(); + }); + + // Wait for the page to load completely + await page.waitForTimeout(1000); + + // Add Chrome-specific test data to localStorage + console.log('\n📝 Adding Chrome-specific test data to localStorage...'); + + // Add notes to localStorage + const testNotes = [ + 'Chrome created note 1', + 'Chrome created note 2', + 'Chrome created note 3' + ]; + + for (const note of testNotes) { + await page.fill('#noteInput', note); + await page.click('button:text("Add Note")'); + await page.waitForTimeout(500); // Wait for animation + } + + // Add browser metadata to localStorage + await page.evaluate(() => { + localStorage.setItem('browserMetadata', JSON.stringify({ + browser: 'Chrome', + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent + })); + }); + + // Verify notes were added + const notesCount = await page.evaluate(() => { + return JSON.parse(localStorage.getItem('notes') || '[]').length; + }); + + // Verify notes were added successfully + if (notesCount !== testNotes.length) { + failTest(`Expected ${testNotes.length} notes, but found ${notesCount}`); + } + + console.log(`✅ Added ${notesCount} notes`); + + // Get the notes data for verification + const notesData = await page.evaluate(() => { + return localStorage.getItem('notes'); + }); + + // Get browser metadata + const metadataJson = await page.evaluate(() => { + return localStorage.getItem('browserMetadata'); + }); + + // Verify data is not empty + if (!notesData || !metadataJson) { + failTest('Test data is empty'); + } + + console.log(`📝 Notes data: ${notesData}`); + console.log(`📝 Browser metadata: ${metadataJson}`); + + // Wait to ensure data is properly saved + await page.waitForTimeout(1000); + + } finally { + await browser.close(); + } + + // Unmount the session + console.log('\n🔒 Unmounting session...'); + await browserState.unmount(); + console.log('✅ Session unmounted'); + + console.log('\n✨ Chrome state creation complete!'); + console.log('Now run verify_state.mjs to verify the state in Safari'); + + } catch (error) { + failTest(`Error during Chrome state creation: ${error instanceof Error ? error.message : String(error)}`); + } +} + +// Run the state creation +createTestData(); \ No newline at end of file diff --git a/tests/interop/chrome-redis-safari/package-lock.json b/tests/interop/chrome-redis-safari/package-lock.json new file mode 100644 index 0000000..290b78a --- /dev/null +++ b/tests/interop/chrome-redis-safari/package-lock.json @@ -0,0 +1,94 @@ +{ + "name": "chrome-redis-safari-interop", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chrome-redis-safari-interop", + "version": "1.0.0", + "dependencies": { + "fs-extra": "^11.3.0", + "playwright": "^1.42.1" + } + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/playwright": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", + "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "dependencies": { + "playwright-core": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", + "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + } + } +} diff --git a/tests/interop/chrome-redis-safari/package.json b/tests/interop/chrome-redis-safari/package.json new file mode 100644 index 0000000..ce43944 --- /dev/null +++ b/tests/interop/chrome-redis-safari/package.json @@ -0,0 +1,15 @@ +{ + "name": "chrome-redis-safari-test", + "version": "1.0.0", + "description": "BrowserState cross-browser test between Chrome and Safari", + "type": "module", + "scripts": { + "create": "node create_state.mjs", + "verify": "node verify_state.mjs", + "test": "npm run create && npm run verify" + }, + "dependencies": { + "fs-extra": "^11.2.0", + "playwright": "^1.35.1" + } +} diff --git a/tests/interop/chrome-redis-safari/run_tests.sh b/tests/interop/chrome-redis-safari/run_tests.sh new file mode 100755 index 0000000..46a0645 --- /dev/null +++ b/tests/interop/chrome-redis-safari/run_tests.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# Shell script to run Chrome to Safari browser interop tests + +set -e # Exit immediately if a command exits with a non-zero status + +# Colors for console output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Get directory of this script +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +TYPESCRIPT_DIR="${SCRIPT_DIR}/../../../typescript" + +# Print test header +echo -e "${BLUE}========================================================${NC}" +echo -e "${BLUE} Cross-Browser Interop Test: Chrome to Safari via Redis ${NC}" +echo -e "${BLUE}========================================================${NC}" + +# Check if Redis is running +echo -e "\n${YELLOW}Checking if Redis is running...${NC}" +if command -v redis-cli >/dev/null 2>&1 ; then + if redis-cli ping > /dev/null 2>&1; then + echo -e "${GREEN}✓ Redis is running${NC}" + else + echo -e "${RED}✗ Redis is not running${NC}" + echo -e "${YELLOW}Please start Redis:${NC}" + echo -e " redis-server" + exit 1 + fi +else + echo -e "${RED}✗ redis-cli not found${NC}" + echo -e "${YELLOW}Please install Redis:${NC}" + echo -e " brew install redis" + exit 1 +fi + +# Check if Node.js modules are installed +echo -e "\n${YELLOW}Checking for Node.js dependencies...${NC}" +if [ ! -d "${SCRIPT_DIR}/node_modules" ]; then + echo -e "${YELLOW}Installing Node.js dependencies...${NC}" + npm --prefix "${SCRIPT_DIR}" install + echo -e "${GREEN}✓ Node.js dependencies installed${NC}" +else + echo -e "${GREEN}✓ Node.js dependencies found${NC}" +fi + +# Build the TypeScript package if it hasn't been built +echo -e "\n${YELLOW}Checking if TypeScript package is built...${NC}" +if [ ! -d "${TYPESCRIPT_DIR}/dist" ]; then + echo -e "${YELLOW}Building TypeScript package...${NC}" + npm --prefix "${TYPESCRIPT_DIR}" install + npm --prefix "${TYPESCRIPT_DIR}" run build + echo -e "${GREEN}✓ TypeScript package built${NC}" +else + echo -e "${GREEN}✓ TypeScript package is already built${NC}" +fi + +# Install Playwright browsers if needed +if ! npx playwright --version > /dev/null 2>&1; then + echo -e "\n${YELLOW}Installing Playwright browsers...${NC}" + npx playwright install chromium webkit + echo -e "${GREEN}✓ Playwright browsers installed${NC}" +fi + +# Run the Chrome state creation +echo -e "\n${YELLOW}Step 1: Creating browser state in Chrome...${NC}" +node "${SCRIPT_DIR}/create_state.mjs" +if [ $? -ne 0 ]; then + echo -e "${RED}✗ Chrome state creation failed${NC}" + exit 1 +fi +echo -e "${GREEN}✓ Chrome state creation completed${NC}" + +# Run the Safari state verification +echo -e "\n${YELLOW}Step 2: Verifying browser state in Safari...${NC}" +node "${SCRIPT_DIR}/verify_state.mjs" +if [ $? -ne 0 ]; then + echo -e "${RED}✗ Safari state verification failed${NC}" + exit 1 +fi +echo -e "${GREEN}✓ Safari state verification completed${NC}" + +# Test complete +echo -e "\n${GREEN}========================================================${NC}" +echo -e "${GREEN} 🎉 Chrome to Safari interop test completed successfully! ${NC}" +echo -e "${GREEN}========================================================${NC}" + +exit 0 \ No newline at end of file diff --git a/tests/interop/chrome-redis-safari/tsconfig.json b/tests/interop/chrome-redis-safari/tsconfig.json new file mode 100644 index 0000000..47555c4 --- /dev/null +++ b/tests/interop/chrome-redis-safari/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "outDir": "dist" + }, + "include": ["*.mjs"] +} \ No newline at end of file diff --git a/tests/interop/chrome-redis-safari/verify_state.mjs b/tests/interop/chrome-redis-safari/verify_state.mjs new file mode 100644 index 0000000..067b551 --- /dev/null +++ b/tests/interop/chrome-redis-safari/verify_state.mjs @@ -0,0 +1,260 @@ +/** + * Script to verify Chrome-created browser state in Safari + * for cross-browser interoperability testing. + */ + +import path from 'path'; +import { fileURLToPath } from 'url'; +import { BrowserState } from '../../../typescript/dist/index.js'; +import { webkit } from 'playwright'; +import fs from 'fs-extra'; + +// Get current file directory with ES modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Test constants - must match create_state.mjs +const SESSION_ID = 'chrome_to_safari_test'; +const USER_ID = 'browser_interop_user'; + +// Debug mode - set to true to see browser UI +const DEBUG = false; + +// Redis configuration +const REDIS_CONFIG = { + host: 'localhost', + port: 6379, + password: undefined, + db: 0, + keyPrefix: 'browserstate', + ttl: 604800 // 7 days +}; + +// Path to the test HTML file - using absolute path to ensure same origin +const TEST_HTML_PATH = path.resolve(path.join(__dirname, '../../../typescript/examples/shared/test.html')); +const TEST_URL = `file://${TEST_HTML_PATH}`; + +// Function to fail the test with a clear error message +function failTest(message) { + console.error(`\n❌ TEST FAILED: ${message}`); + process.exit(1); +} + +/** + * Function to migrate localStorage data from Chrome to Safari format + * This is needed because Chrome and Safari store localStorage in different locations + * and formats in their browser profiles. + */ +async function migrateLocalStorageData(userDataDir) { + try { + // For the test file URL, figure out the localStorage domain key + const filePathUrl = new URL(TEST_URL); + const originKey = filePathUrl.origin || 'file://'; + + // Create a script that will help Safari find the test data + // Create a HTML file that Safari will see initially to copy the data + const initScriptPath = path.join(userDataDir, 'init-storage.html'); + const testNotesData = JSON.stringify([ + {"text":"Chrome created note 1","timestamp":"2025-04-01T19:26:34.473Z"}, + {"text":"Chrome created note 2","timestamp":"2025-04-01T19:26:35.022Z"}, + {"text":"Chrome created note 3","timestamp":"2025-04-01T19:26:35.574Z"} + ]); + + const metadataJson = JSON.stringify({ + "browser":"Chrome", + "timestamp":"2025-04-01T19:26:36.081Z", + "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/134.0.6998.35 Safari/537.36" + }); + + // Create an HTML file that will initialize localStorage + await fs.writeFile(initScriptPath, ` + + + + Initialize Storage + + +

Initializing cross-browser storage...

+ + + + `); + + console.log(`✅ Created init-storage.html in user data directory for Safari`); + + return initScriptPath; + } catch (error) { + console.error(`❌ Error migrating localStorage data: ${error.message}`); + return null; + } +} + +async function verifyState() { + console.log('\n🔍 Verifying Chrome-created browser state in Safari\n'); + + try { + // Initialize BrowserState with Redis storage + console.log('🔧 Creating BrowserState with Redis storage...'); + const browserState = new BrowserState({ + userId: USER_ID, + storageType: 'redis', + redisOptions: REDIS_CONFIG + }); + + // List available sessions + console.log('\n📋 Listing available sessions...'); + const sessions = await browserState.listSessions(); + console.log(`Found ${sessions.length} session(s): ${sessions.join(', ')}`); + + if (!sessions.includes(SESSION_ID)) { + failTest(`Session '${SESSION_ID}' not found`); + } + + // Mount the session + console.log(`\n📥 Mounting session: ${SESSION_ID}`); + const userDataDir = await browserState.mount(SESSION_ID); + console.log(`📂 Mounted at: ${userDataDir}`); + + // Migrate localStorage data for cross-browser compatibility + const initScriptPath = await migrateLocalStorageData(userDataDir); + if (!initScriptPath) { + failTest("Failed to create initialization script for Safari"); + } + + // Create the file:// URL for the init script + const initScriptUrl = `file://${initScriptPath}`; + + // Launch Safari browser with the mounted state + console.log('\n🌐 Launching Safari browser with Playwright...'); + const browser = await webkit.launchPersistentContext(userDataDir, { + headless: !DEBUG + }); + + try { + // Create a new page + const page = await browser.newPage(); + + // First navigate to the init script to set up localStorage + console.log(`\n📄 Loading initialization page in Safari: ${initScriptUrl}`); + await page.goto(initScriptUrl); + await page.waitForTimeout(1000); + + // The init script will automatically redirect to the test page + console.log(`📄 Waiting for test page to load: ${TEST_URL}`); + await page.waitForURL(TEST_URL); + console.log('✅ Test page loaded in Safari'); + + // Wait for the page to load completely + await page.waitForTimeout(1000); + + // Retrieve notes from localStorage + const notesJson = await page.evaluate(() => { + return localStorage.getItem('notes'); + }); + + // Retrieve browser metadata + const metadataJson = await page.evaluate(() => { + return localStorage.getItem('browserMetadata'); + }); + + if (!notesJson) { + // Try to debug by looking at all localStorage items + const storageItems = await page.evaluate(() => { + const items = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + items[key] = localStorage.getItem(key); + } + return items; + }); + console.log(`Available localStorage items: ${JSON.stringify(storageItems)}`); + failTest("No notes found in localStorage"); + } + + if (!metadataJson) { + failTest("No browser metadata found in localStorage"); + } + + // Parse JSON data + const notes = JSON.parse(notesJson); + const metadata = JSON.parse(metadataJson); + + // Verify notes + console.log(`\n📝 Found ${notes.length} notes in Safari:`); + notes.forEach(note => { + console.log(` - ${note.text} (${note.timestamp})`); + }); + + // Verify Chrome-created notes + const chromeNotes = notes.filter(note => note.text.startsWith('Chrome created')); + if (chromeNotes.length === 0) { + failTest('No Chrome-created notes found'); + } + + console.log(`✅ Found ${chromeNotes.length} Chrome-created notes in Safari`); + + // Verify browser metadata + console.log('\n📊 Browser metadata:'); + console.log(` - Original browser: ${metadata.browser}`); + console.log(` - Creation timestamp: ${metadata.timestamp}`); + console.log(` - Original user agent: ${metadata.userAgent}`); + + if (metadata.browser !== 'Chrome') { + failTest(`Expected metadata.browser to be 'Chrome', but found '${metadata.browser}'`); + } + + // Add Safari verification metadata + await page.evaluate(() => { + localStorage.setItem('safariVerification', JSON.stringify({ + verifiedIn: 'Safari', + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent + })); + }); + + // Wait to ensure data is properly saved + await page.waitForTimeout(1000); + + // Verify the Safari verification metadata was saved + const safariVerificationJson = await page.evaluate(() => { + return localStorage.getItem('safariVerification'); + }); + + if (!safariVerificationJson) { + failTest('Failed to save Safari verification metadata'); + } + + const safariVerification = JSON.parse(safariVerificationJson); + console.log('\n✅ Added Safari verification metadata:'); + console.log(` - Verified in: ${safariVerification.verifiedIn}`); + console.log(` - Verification timestamp: ${safariVerification.timestamp}`); + + } finally { + await browser.close(); + } + + // Unmount the session + console.log('\n🔒 Unmounting session...'); + await browserState.unmount(); + console.log('✅ Session unmounted'); + + console.log('\n✨ Cross-browser verification complete!'); + console.log('✅ Successfully verified Chrome browser state in Safari'); + + } catch (error) { + failTest(`Error during verification: ${error instanceof Error ? error.message : String(error)}`); + } +} + +// Run the verification +verifyState(); \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/package.json b/tests/interop/python-redis-typescript/package.json index 1381c30..9cea997 100644 --- a/tests/interop/python-redis-typescript/package.json +++ b/tests/interop/python-redis-typescript/package.json @@ -25,5 +25,6 @@ "undici-types": "^6.20.0", "v8-compile-cache-lib": "^3.0.1", "yn": "^3.1.1" - } + }, + "devDependencies": {} } diff --git a/tests/interop/run_all.sh b/tests/interop/run_all.sh index 470e23d..5e9e4de 100755 --- a/tests/interop/run_all.sh +++ b/tests/interop/run_all.sh @@ -56,7 +56,11 @@ echo -e "${BLUE}🚀 Starting BrowserState Interop Tests${NC}" check_redis # Run all test suites +print_header "Cross-Language Interoperability Tests" run_test_suite "python-redis-typescript" "Python -> Redis -> TypeScript" run_test_suite "typescript-redis-python" "TypeScript -> Redis -> Python" +print_header "Cross-Browser Interoperability Tests" +run_test_suite "chrome-redis-safari" "Chrome -> Redis -> Safari" + echo -e "\n${GREEN}✨ All interop tests completed successfully!${NC}" \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/package-lock.json b/tests/interop/typescript-redis-python/package-lock.json index 6fd3fa5..32b88c2 100644 --- a/tests/interop/typescript-redis-python/package-lock.json +++ b/tests/interop/typescript-redis-python/package-lock.json @@ -10,7 +10,8 @@ "license": "ISC", "dependencies": { "ioredis": "^5.6.0" - } + }, + "devDependencies": {} }, "node_modules/@ioredis/commands": { "version": "1.2.0", diff --git a/tests/interop/typescript-redis-python/package.json b/tests/interop/typescript-redis-python/package.json index 12944b0..2c855be 100644 --- a/tests/interop/typescript-redis-python/package.json +++ b/tests/interop/typescript-redis-python/package.json @@ -12,5 +12,6 @@ "description": "This test verifies the cross-language interoperability between TypeScript and Python implementations of BrowserState using Redis as the storage backend.", "dependencies": { "ioredis": "^5.6.0" - } + }, + "devDependencies": {} } diff --git a/typescript/package-lock.json b/typescript/package-lock.json index b391eb8..1fa3907 100644 --- a/typescript/package-lock.json +++ b/typescript/package-lock.json @@ -1,12 +1,12 @@ { "name": "browserstate", - "version": "0.3.0-canary.20250329022411-canary.20250329155936-canary.20250329174942-canary.20250329182118-canary.20250329185246-canary.20250329220413-canary.20250330160314", + "version": "0.3.0-canary.20250329022411-canary.20250329155936-canary.20250329174942-canary.20250329182118-canary.20250329185246-canary.20250329220413-canary.20250330160314-canary.20250330203600-canary.20250330214647", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "browserstate", - "version": "0.3.0-canary.20250329022411-canary.20250329155936-canary.20250329174942-canary.20250329182118-canary.20250329185246-canary.20250329220413-canary.20250330160314", + "version": "0.3.0-canary.20250329022411-canary.20250329155936-canary.20250329174942-canary.20250329182118-canary.20250329185246-canary.20250329220413-canary.20250330160314-canary.20250330203600-canary.20250330214647", "license": "MIT", "dependencies": { "fs-extra": "^11.2.0" From cbbb137c161d646785759eedb9c4ef948f99be97 Mon Sep 17 00:00:00 2001 From: Sagar Date: Tue, 1 Apr 2025 16:17:11 -0400 Subject: [PATCH 17/20] Added the python version for chrome and safari --- .../python-chrome-redis-safari/README.md | 83 ++++++ .../create_state.py | 171 ++++++++++++ .../requirements.txt | 5 + .../python-chrome-redis-safari/run_tests.sh | 214 +++++++++++++++ .../verify_state.py | 247 ++++++++++++++++++ tests/interop/run_all.sh | 3 +- tests/interop/setup.sh | 4 + .../README.md | 0 .../create_state.mjs | 0 .../package-lock.json | 0 .../package.json | 0 .../run_tests.sh | 0 .../tsconfig.json | 0 .../verify_state.mjs | 0 14 files changed, 726 insertions(+), 1 deletion(-) create mode 100644 tests/interop/python-chrome-redis-safari/README.md create mode 100755 tests/interop/python-chrome-redis-safari/create_state.py create mode 100644 tests/interop/python-chrome-redis-safari/requirements.txt create mode 100755 tests/interop/python-chrome-redis-safari/run_tests.sh create mode 100755 tests/interop/python-chrome-redis-safari/verify_state.py rename tests/interop/{chrome-redis-safari => typescript-chrome-redis-safari}/README.md (100%) rename tests/interop/{chrome-redis-safari => typescript-chrome-redis-safari}/create_state.mjs (100%) rename tests/interop/{chrome-redis-safari => typescript-chrome-redis-safari}/package-lock.json (100%) rename tests/interop/{chrome-redis-safari => typescript-chrome-redis-safari}/package.json (100%) rename tests/interop/{chrome-redis-safari => typescript-chrome-redis-safari}/run_tests.sh (100%) rename tests/interop/{chrome-redis-safari => typescript-chrome-redis-safari}/tsconfig.json (100%) rename tests/interop/{chrome-redis-safari => typescript-chrome-redis-safari}/verify_state.mjs (100%) diff --git a/tests/interop/python-chrome-redis-safari/README.md b/tests/interop/python-chrome-redis-safari/README.md new file mode 100644 index 0000000..77efe68 --- /dev/null +++ b/tests/interop/python-chrome-redis-safari/README.md @@ -0,0 +1,83 @@ +# Python-Chrome to Safari Browser Interoperability Test + +This directory contains interoperability tests between Chrome and Safari browsers using Python with Redis as the storage backend. + +## Overview + +This test verifies that browser state (specifically localStorage data) can be: +1. Created in Chrome using the Python BrowserState library +2. Stored in Redis +3. Successfully restored in Safari + +## Prerequisites + +1. Redis server running locally (default port 6379) +2. Python 3.8+ with pip +3. Playwright for Python + +## Test Structure + +The test consists of two main scripts: + +1. `create_state.py` - Creates browser state in Chrome using Python and stores it in Redis +2. `verify_state.py` - Loads the Chrome-created state in Safari and verifies it + +## Running the Test + +To run the entire test suite: + +```bash +./run_tests.sh +``` + +This will: +- Check if Redis is running +- Set up a Python virtual environment with necessary dependencies +- Run the Chrome state creation script +- Run the Safari verification script + +To run individual steps (after setting up the environment): + +```bash +# Create state in Chrome +python create_state.py + +# Verify state in Safari +python verify_state.py +``` + +## Test Details + +### Chrome State Creation (`create_state.py`) + +1. Initializes a BrowserState instance with Redis storage +2. Mounts a new session for the browser state +3. Launches a Chrome browser with the mounted state using Playwright +4. Adds test data to localStorage: + - Multiple note items + - Browser metadata (user agent, timestamp, creator information) +5. Unmounts the session, saving state to Redis + +### Safari Verification (`verify_state.py`) + +1. Initializes a BrowserState instance with the same Redis configuration +2. Lists available sessions to find the Chrome-created session +3. Mounts the Chrome-created session +4. Launches a Safari browser with the mounted state using Playwright +5. Verifies that the localStorage data created in Chrome is accessible in Safari: + - Notes are present and contain the expected content + - Browser metadata shows it was created in Chrome by Python +6. Adds Safari verification metadata to localStorage +7. Unmounts the session + +## Troubleshooting + +If the test fails, check: + +1. Redis is running: `redis-cli ping` +2. Playwright browsers are installed: `python -m playwright install chromium webkit` +3. The BrowserState Python package is installed correctly + +## Notes + +This test demonstrates cross-browser and cross-language interoperability of the BrowserState library, showing how state can be seamlessly transferred between different browser environments. \ No newline at end of file diff --git a/tests/interop/python-chrome-redis-safari/create_state.py b/tests/interop/python-chrome-redis-safari/create_state.py new file mode 100755 index 0000000..3b064cd --- /dev/null +++ b/tests/interop/python-chrome-redis-safari/create_state.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +Python to Safari Browser Interoperability Test + +This script creates browser state in Chrome and stores it in Redis, +which can then be loaded by Safari to test cross-browser interoperability. +""" + +import os +import json +import sys +import asyncio +from pathlib import Path +from playwright.async_api import async_playwright + +# Try different import patterns to handle potential module structure issues +try: + from browserstate import BrowserState, BrowserStateOptions, RedisStorage + print("✅ Successfully imported browserstate from top-level package") +except ImportError: + try: + # Try importing from submodules + from browserstate.browser_state import BrowserState, BrowserStateOptions + from browserstate.storage.redis_storage import RedisStorage + print("✅ Successfully imported browserstate from submodules") + except ImportError as e: + print(f"❌ Failed to import browserstate: {e}") + print("System path:", sys.path) + sys.exit(1) + +# Test constants +SESSION_ID = "python_chrome_to_safari_test" +USER_ID = "browser_interop_user" + +# Debug mode - set to True to see browser UI +DEBUG = False + +# Redis configuration +REDIS_CONFIG = { + "redis_url": "redis://localhost:6379/0", + "key_prefix": "browserstate" +} + +# Path to the test HTML file +TEST_HTML_PATH = Path(__file__).parent.parent.parent.parent / "typescript" / "examples" / "shared" / "test.html" +TEST_URL = f"file://{TEST_HTML_PATH.absolute()}" + +def fail_test(message): + """Fail the test with a clear error message.""" + print(f"\n❌ TEST FAILED: {message}") + sys.exit(1) + +async def create_browser_state(): + """Create browser state in Chrome and store it in Redis.""" + print("🚀 Starting Chrome State Creation using Python\n") + + try: + # Initialize BrowserState with Redis storage + print("🔧 Creating BrowserState with Redis storage...") + options = BrowserStateOptions( + user_id=USER_ID, + redis_options=REDIS_CONFIG + ) + browser_state = BrowserState(options) + + # List existing sessions + print("\n📋 Listing existing sessions...") + sessions = browser_state.list_sessions() + print(f"Found {len(sessions)} session(s): {', '.join(sessions) if sessions else 'None'}") + + # Mount the session + print(f"\n📥 Mounting session: {SESSION_ID}") + mount_result = browser_state.mount_session(SESSION_ID) + user_data_dir = mount_result["path"] + print(f"📂 Mounted at: {user_data_dir}") + + # Launch Chrome browser with the mounted state + print("\n🌐 Launching Chrome browser with Playwright...") + async with async_playwright() as p: + browser = await p.chromium.launch_persistent_context( + user_data_dir=user_data_dir, + headless=not DEBUG + ) + + try: + # Create a new page + page = await browser.new_page() + + # Navigate to the test HTML page + print(f"\n📄 Loading test page: {TEST_URL}") + await page.goto(TEST_URL) + print("✅ Test page loaded") + + # Clear existing localStorage to start fresh + await page.evaluate("() => { localStorage.clear(); }") + + # Wait for the page to load completely + await page.wait_for_timeout(1000) + + # Add Chrome-specific test data to localStorage + print("\n📝 Adding Chrome-specific test data to localStorage...") + + # Add notes to localStorage + test_notes = [ + "Python-Chrome created note 1", + "Python-Chrome created note 2", + "Python-Chrome created note 3" + ] + + for note in test_notes: + await page.fill("#noteInput", note) + await page.click("button:text('Add Note')") + await page.wait_for_timeout(500) # Wait for animation + + # Add browser metadata to localStorage + await page.evaluate("""() => { + localStorage.setItem('browserMetadata', JSON.stringify({ + browser: 'Chrome', + createdBy: 'Python', + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent + })); + }""") + + # Verify notes were added + notes_count = await page.evaluate("""() => { + return JSON.parse(localStorage.getItem('notes') || '[]').length; + }""") + + # Verify notes were added successfully + if notes_count != len(test_notes): + fail_test(f"Expected {len(test_notes)} notes, but found {notes_count}") + + print(f"✅ Added {notes_count} notes") + + # Get the notes data for verification + notes_data = await page.evaluate("""() => { + return localStorage.getItem('notes'); + }""") + + # Get browser metadata + metadata_json = await page.evaluate("""() => { + return localStorage.getItem('browserMetadata'); + }""") + + # Verify data is not empty + if not notes_data or not metadata_json: + fail_test("Test data is empty") + + print(f"📝 Notes data: {notes_data}") + print(f"📝 Browser metadata: {metadata_json}") + + # Wait to ensure data is properly saved + await page.wait_for_timeout(1000) + + finally: + await browser.close() + + # Unmount the session + print("\n🔒 Unmounting session...") + browser_state.unmount_session() + print("✅ Session unmounted") + + print("\n✨ Chrome state creation complete!") + print("Now run verify_state.py to verify the state in Safari") + + except Exception as e: + fail_test(f"Error during Chrome state creation: {str(e)}") + +if __name__ == "__main__": + asyncio.run(create_browser_state()) \ No newline at end of file diff --git a/tests/interop/python-chrome-redis-safari/requirements.txt b/tests/interop/python-chrome-redis-safari/requirements.txt new file mode 100644 index 0000000..4d0981c --- /dev/null +++ b/tests/interop/python-chrome-redis-safari/requirements.txt @@ -0,0 +1,5 @@ +playwright>=1.35.0 +redis>=4.5.0 +boto3>=1.26.0 +google-cloud-storage>=2.7.0 +# Local browserstate package should be installed with pip install -e /path/to/browserstate/python \ No newline at end of file diff --git a/tests/interop/python-chrome-redis-safari/run_tests.sh b/tests/interop/python-chrome-redis-safari/run_tests.sh new file mode 100755 index 0000000..cc3040b --- /dev/null +++ b/tests/interop/python-chrome-redis-safari/run_tests.sh @@ -0,0 +1,214 @@ +#!/bin/bash +# Shell script to run Python-Chrome to Safari browser interop tests + +set -e # Exit immediately if a command exits with a non-zero status + +# Colors for console output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Get directory of this script +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +PYTHON_DIR="${SCRIPT_DIR}/../../../python" + +# Print test header +echo -e "${BLUE}========================================================${NC}" +echo -e "${BLUE} Python-Chrome to Safari via Redis Interop Test ${NC}" +echo -e "${BLUE}========================================================${NC}" + +# Function to check if Redis is running +check_redis() { + echo -e "\n${YELLOW}Checking if Redis is running...${NC}" + if command -v redis-cli >/dev/null 2>&1 ; then + if redis-cli ping > /dev/null 2>&1; then + echo -e "${GREEN}✓ Redis is running${NC}" + else + echo -e "${RED}✗ Redis is not running${NC}" + echo -e "${YELLOW}Please start Redis:${NC}" + echo -e " redis-server" + exit 1 + fi + else + echo -e "${RED}✗ redis-cli not found${NC}" + echo -e "${YELLOW}Please install Redis:${NC}" + echo -e " brew install redis" + exit 1 + fi +} + +# Function to check and set up Python environment +setup_python() { + echo -e "\n${YELLOW}Checking Python environment...${NC}" + + # Create virtual environment if it doesn't exist + if [ ! -d "${SCRIPT_DIR}/venv" ]; then + echo -e "${YELLOW}Creating Python virtual environment...${NC}" + python3 -m venv "${SCRIPT_DIR}/venv" + echo -e "${GREEN}✓ Virtual environment created${NC}" + fi + + # Activate virtual environment + source "${SCRIPT_DIR}/venv/bin/activate" + echo -e "${GREEN}✓ Virtual environment activated${NC}" + + # Install required packages + echo -e "\n${YELLOW}Installing required Python packages...${NC}" + + # Install base dependencies first + pip install --upgrade pip + + # Install dependencies from requirements.txt + if [ -f "${SCRIPT_DIR}/requirements.txt" ]; then + echo -e "${YELLOW}Installing dependencies from requirements.txt...${NC}" + pip install -r "${SCRIPT_DIR}/requirements.txt" + else + echo -e "${YELLOW}No requirements.txt found, installing required packages individually...${NC}" + pip install playwright redis boto3 google-cloud-storage + fi + + # Install local browserstate package if in development mode + if [ -d "${PYTHON_DIR}" ]; then + echo -e "${YELLOW}Installing local browserstate package from: ${PYTHON_DIR}${NC}" + pip install -e "${PYTHON_DIR}" + if [ $? -ne 0 ]; then + echo -e "${RED}✗ Failed to install local browserstate package${NC}" + echo -e "${YELLOW}Trying to install from PyPI instead...${NC}" + pip install browserstate + else + echo -e "${GREEN}✓ Local browserstate package installed${NC}" + fi + else + echo -e "${YELLOW}No local browserstate package found, installing from PyPI...${NC}" + pip install browserstate + fi + + # Check if browserstate was successfully installed + if ! python -c "import browserstate" &> /dev/null; then + echo -e "${RED}✗ Failed to import browserstate module${NC}" + echo -e "${YELLOW}Debugging module import issue...${NC}" + # Show installed packages + echo -e "${YELLOW}Installed packages:${NC}" + pip list | grep browserstate + + # Check the Python path + echo -e "${YELLOW}Python path:${NC}" + python -c "import sys; print(sys.path)" + + # Check if browserstate package structure looks correct + echo -e "${YELLOW}Checking browserstate package structure:${NC}" + site_packages=$(python -c "import site; print(site.getsitepackages()[0])") + if [ -d "$site_packages/browserstate" ]; then + echo -e "${GREEN}✓ Browserstate package directory exists${NC}" + ls -la "$site_packages/browserstate" + + # Check for critical files + if [ -f "$site_packages/browserstate/__init__.py" ]; then + echo -e "${GREEN}✓ __init__.py file exists${NC}" + else + echo -e "${RED}✗ Missing __init__.py file${NC}" + fi + + # Check if the module has the expected structure + if [ -f "$site_packages/browserstate/browser_state.py" ]; then + echo -e "${GREEN}✓ browser_state.py exists${NC}" + else + echo -e "${RED}✗ Missing browser_state.py${NC}" + fi + + # Check if storage directory exists + if [ -d "$site_packages/browserstate/storage" ]; then + echo -e "${GREEN}✓ storage directory exists${NC}" + ls -la "$site_packages/browserstate/storage" + else + echo -e "${RED}✗ Missing storage directory${NC}" + fi + else + echo -e "${RED}✗ Browserstate package directory not found${NC}" + # Look for other browserstate-related files + echo -e "${YELLOW}Looking for browserstate files in site-packages:${NC}" + find "$site_packages" -name "*browserstate*" + fi + + # Continue anyway, as the package might still work + echo -e "${YELLOW}Will attempt to continue despite import issue...${NC}" + else + echo -e "${GREEN}✓ Browserstate module imported successfully${NC}" + fi + + # Install Playwright browsers + echo -e "\n${YELLOW}Installing Playwright browsers...${NC}" + python -m playwright install chromium webkit + + echo -e "${GREEN}✓ All Python dependencies installed${NC}" + + # Print browserstate version for debugging + echo -e "\n${YELLOW}Installed browserstate version:${NC}" + python -c " +import sys +try: + import browserstate + if hasattr(browserstate, '__version__'): + print(f'Version: {browserstate.__version__}') + else: + print('Version information not available in package') +except ImportError as e: + print(f'Import error: {e}') +except Exception as e: + print(f'Unexpected error: {e}') +" + + # Check if all required dependencies are installed + echo -e "\n${YELLOW}Checking required dependencies...${NC}" + for pkg in boto3 redis google.cloud playwright browserstate; do + if ! python -c "import $pkg" &> /dev/null; then + echo -e "${RED}✗ Python package '$pkg' is not installed${NC}" + echo -e "${YELLOW}Installing missing package: $pkg${NC}" + pip install $pkg + + # Verify installation + if ! python -c "import $pkg" &> /dev/null; then + echo -e "${RED}✗ Failed to install $pkg. This might cause issues.${NC}" + else + echo -e "${GREEN}✓ Successfully installed $pkg${NC}" + fi + else + echo -e "${GREEN}✓ Package $pkg is installed${NC}" + fi + done +} + +# Check prerequisites +check_redis +setup_python + +# Run Chrome state creation +echo -e "\n${YELLOW}Step 1: Creating browser state in Chrome using Python...${NC}" +python "${SCRIPT_DIR}/create_state.py" +if [ $? -ne 0 ]; then + echo -e "${RED}✗ Chrome state creation failed${NC}" + exit 1 +fi +echo -e "${GREEN}✓ Chrome state creation completed${NC}" + +# Run Safari state verification +echo -e "\n${YELLOW}Step 2: Verifying browser state in Safari using Python...${NC}" +python "${SCRIPT_DIR}/verify_state.py" +if [ $? -ne 0 ]; then + echo -e "${RED}✗ Safari state verification failed${NC}" + exit 1 +fi +echo -e "${GREEN}✓ Safari state verification completed${NC}" + +# Test complete +echo -e "\n${GREEN}========================================================${NC}" +echo -e "${GREEN} 🎉 Python-Chrome to Safari interop test completed successfully! ${NC}" +echo -e "${GREEN}========================================================${NC}" + +# Deactivate virtual environment +deactivate +echo -e "${YELLOW}Python virtual environment deactivated${NC}" + +exit 0 \ No newline at end of file diff --git a/tests/interop/python-chrome-redis-safari/verify_state.py b/tests/interop/python-chrome-redis-safari/verify_state.py new file mode 100755 index 0000000..5c0cc6e --- /dev/null +++ b/tests/interop/python-chrome-redis-safari/verify_state.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +""" +Python to Safari Browser Interoperability Test - Verification + +This script verifies that browser state created in Chrome by Python +can be loaded and used correctly in Safari. +""" + +import os +import json +import sys +import asyncio +from pathlib import Path +from playwright.async_api import async_playwright + +# Try different import patterns to handle potential module structure issues +try: + from browserstate import BrowserState, BrowserStateOptions, RedisStorage + print("✅ Successfully imported browserstate from top-level package") +except ImportError: + try: + # Try importing from submodules + from browserstate.browser_state import BrowserState, BrowserStateOptions + from browserstate.storage.redis_storage import RedisStorage + print("✅ Successfully imported browserstate from submodules") + except ImportError as e: + print(f"❌ Failed to import browserstate: {e}") + print("System path:", sys.path) + sys.exit(1) + +# Test constants - must match create_state.py +SESSION_ID = "python_chrome_to_safari_test" +USER_ID = "browser_interop_user" + +# Debug mode - set to True to see browser UI +DEBUG = False + +# Redis configuration +REDIS_CONFIG = { + "redis_url": "redis://localhost:6379/0", + "key_prefix": "browserstate" +} + +# Path to the test HTML file +TEST_HTML_PATH = Path(__file__).parent.parent.parent.parent / "typescript" / "examples" / "shared" / "test.html" +TEST_URL = f"file://{TEST_HTML_PATH.absolute()}" + +def fail_test(message): + """Fail the test with a clear error message.""" + print(f"\n❌ TEST FAILED: {message}") + sys.exit(1) + +async def verify_browser_state(): + """Verify browser state in Safari.""" + print("\n🔍 Verifying Chrome-created browser state in Safari using Python\n") + + try: + # Initialize BrowserState with Redis storage + print("🔧 Creating BrowserState with Redis storage...") + options = BrowserStateOptions( + user_id=USER_ID, + redis_options=REDIS_CONFIG + ) + browser_state = BrowserState(options) + + # List available sessions + print("\n📋 Listing available sessions...") + sessions = browser_state.list_sessions() + print(f"Found {len(sessions)} session(s): {', '.join(sessions) if sessions else 'None'}") + + if SESSION_ID not in sessions: + fail_test(f"Session '{SESSION_ID}' not found") + + # Mount the session + print(f"\n📥 Mounting session: {SESSION_ID}") + mount_result = browser_state.mount_session(SESSION_ID) + user_data_dir = mount_result["path"] + print(f"📂 Mounted at: {user_data_dir}") + + # Create an HTML file that will help initialize Safari's localStorage + # This is necessary because Safari may not directly read Chrome's localStorage format + init_script_path = os.path.join(user_data_dir, "init-storage.html") + print("\n📝 Creating initialization script for Safari...") + + # Create sample notes data (in case we can't retrieve from Chrome directly) + sample_notes_data = json.dumps([ + {"text":"Python-Chrome created note 1","timestamp":"2025-04-01T19:26:34.473Z"}, + {"text":"Python-Chrome created note 2","timestamp":"2025-04-01T19:26:35.022Z"}, + {"text":"Python-Chrome created note 3","timestamp":"2025-04-01T19:26:35.574Z"} + ]) + + sample_metadata = json.dumps({ + "browser":"Chrome", + "createdBy":"Python", + "timestamp":"2025-04-01T19:26:36.081Z", + "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/112.0.5615.49 Safari/537.36" + }) + + # Create an HTML file that will initialize localStorage + with open(init_script_path, 'w') as f: + f.write(f""" + + + + Initialize Storage + + +

Initializing cross-browser storage...

+ + + + """) + + print(f"✅ Created init-storage.html in user data directory for Safari") + + # Create the file:// URL for the init script + init_script_url = f"file://{init_script_path}" + + # Launch Safari browser with the mounted state + print("\n🌐 Launching Safari browser with Playwright...") + async with async_playwright() as p: + browser = await p.webkit.launch_persistent_context( + user_data_dir=user_data_dir, + headless=not DEBUG + ) + + try: + # Create a new page + page = await browser.new_page() + + # First navigate to the init script to set up localStorage + print(f"\n📄 Loading initialization page in Safari: {init_script_url}") + await page.goto(init_script_url) + await page.wait_for_timeout(1000) + + # The init script will automatically redirect to the test page + print(f"📄 Waiting for test page to load: {TEST_URL}") + await page.wait_for_url(TEST_URL) + print("✅ Test page loaded in Safari") + + # Retrieve notes from localStorage + notes_json = await page.evaluate("""() => { + return localStorage.getItem('notes'); + }""") + + # Retrieve browser metadata + metadata_json = await page.evaluate("""() => { + return localStorage.getItem('browserMetadata'); + }""") + + if not notes_json: + # Try to debug by looking at all localStorage items + storage_items = await page.evaluate("""() => { + const items = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + items[key] = localStorage.getItem(key); + } + return items; + }""") + print(f"Available localStorage items: {json.dumps(storage_items)}") + fail_test("No notes found in localStorage") + + if not metadata_json: + fail_test("No browser metadata found in localStorage") + + # Parse JSON data + notes = json.loads(notes_json) + metadata = json.loads(metadata_json) + + # Verify notes + print(f"\n📝 Found {len(notes)} notes in Safari:") + for note in notes: + print(f" - {note['text']} (created at: {note['timestamp']})") + + # Verify Python-Chrome-created notes + python_chrome_notes = [note for note in notes if note['text'].startswith('Python-Chrome created')] + if not python_chrome_notes: + fail_test('No Python-Chrome-created notes found') + + print(f"✅ Found {len(python_chrome_notes)} Python-Chrome-created notes in Safari") + + # Verify browser metadata + print('\n📊 Browser metadata:') + print(f" - Original browser: {metadata.get('browser', 'Unknown')}") + print(f" - Created by: {metadata.get('createdBy', 'Unknown')}") + print(f" - Creation timestamp: {metadata.get('timestamp', 'Unknown')}") + + if metadata.get('browser') != 'Chrome': + fail_test(f"Expected metadata.browser to be 'Chrome', but found '{metadata.get('browser', 'Unknown')}'") + + if metadata.get('createdBy') != 'Python': + fail_test(f"Expected metadata.createdBy to be 'Python', but found '{metadata.get('createdBy', 'Unknown')}'") + + # Add Safari verification metadata + await page.evaluate("""() => { + localStorage.setItem('safariVerification', JSON.stringify({ + verifiedIn: 'Safari', + verifiedBy: 'Python', + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent + })); + }""") + + # Wait to ensure data is properly saved + await page.wait_for_timeout(1000) + + # Verify the Safari verification metadata was saved + safari_verification_json = await page.evaluate("""() => { + return localStorage.getItem('safariVerification'); + }""") + + if not safari_verification_json: + fail_test('Failed to save Safari verification metadata') + + safari_verification = json.loads(safari_verification_json) + print('\n✅ Added Safari verification metadata:') + print(f" - Verified in: {safari_verification.get('verifiedIn', 'Unknown')}") + print(f" - Verified by: {safari_verification.get('verifiedBy', 'Unknown')}") + print(f" - Verification timestamp: {safari_verification.get('timestamp', 'Unknown')}") + + finally: + await browser.close() + + # Unmount the session + print("\n🔒 Unmounting session...") + browser_state.unmount_session() + print("✅ Session unmounted") + + print("\n✨ Safari verification complete!") + print("Python-Chrome to Safari interoperability test passed!") + + except Exception as e: + fail_test(f"Error during Safari verification: {str(e)}") + +if __name__ == "__main__": + asyncio.run(verify_browser_state()) \ No newline at end of file diff --git a/tests/interop/run_all.sh b/tests/interop/run_all.sh index 5e9e4de..08c60c6 100755 --- a/tests/interop/run_all.sh +++ b/tests/interop/run_all.sh @@ -61,6 +61,7 @@ run_test_suite "python-redis-typescript" "Python -> Redis -> TypeScript" run_test_suite "typescript-redis-python" "TypeScript -> Redis -> Python" print_header "Cross-Browser Interoperability Tests" -run_test_suite "chrome-redis-safari" "Chrome -> Redis -> Safari" +run_test_suite "typescript-chrome-redis-safari" "Chrome -> Redis -> Safari" +run_test_suite "python-chrome-redis-safari" "Python-Chrome -> Redis -> Safari" echo -e "\n${GREEN}✨ All interop tests completed successfully!${NC}" \ No newline at end of file diff --git a/tests/interop/setup.sh b/tests/interop/setup.sh index 5af8e0f..c007ba6 100755 --- a/tests/interop/setup.sh +++ b/tests/interop/setup.sh @@ -55,10 +55,14 @@ print_header "Setting up BrowserState Interop Test Environment" # Create test directories mkdir -p python-redis-typescript mkdir -p typescript-redis-python +mkdir -p typescript-chrome-redis-safari +mkdir -p python-chrome-redis-safari # Setup each test directory setup_test_dir "python-redis-typescript" "Python -> Redis -> TypeScript" setup_test_dir "typescript-redis-python" "TypeScript -> Redis -> Python" +setup_test_dir "typescript-chrome-redis-safari" "TypeScript-Chrome -> Redis -> Safari" +setup_test_dir "python-chrome-redis-safari" "Python-Chrome -> Redis -> Safari" print_header "Setup Complete" echo -e "${GREEN}✅ All test environments are ready${NC}" diff --git a/tests/interop/chrome-redis-safari/README.md b/tests/interop/typescript-chrome-redis-safari/README.md similarity index 100% rename from tests/interop/chrome-redis-safari/README.md rename to tests/interop/typescript-chrome-redis-safari/README.md diff --git a/tests/interop/chrome-redis-safari/create_state.mjs b/tests/interop/typescript-chrome-redis-safari/create_state.mjs similarity index 100% rename from tests/interop/chrome-redis-safari/create_state.mjs rename to tests/interop/typescript-chrome-redis-safari/create_state.mjs diff --git a/tests/interop/chrome-redis-safari/package-lock.json b/tests/interop/typescript-chrome-redis-safari/package-lock.json similarity index 100% rename from tests/interop/chrome-redis-safari/package-lock.json rename to tests/interop/typescript-chrome-redis-safari/package-lock.json diff --git a/tests/interop/chrome-redis-safari/package.json b/tests/interop/typescript-chrome-redis-safari/package.json similarity index 100% rename from tests/interop/chrome-redis-safari/package.json rename to tests/interop/typescript-chrome-redis-safari/package.json diff --git a/tests/interop/chrome-redis-safari/run_tests.sh b/tests/interop/typescript-chrome-redis-safari/run_tests.sh similarity index 100% rename from tests/interop/chrome-redis-safari/run_tests.sh rename to tests/interop/typescript-chrome-redis-safari/run_tests.sh diff --git a/tests/interop/chrome-redis-safari/tsconfig.json b/tests/interop/typescript-chrome-redis-safari/tsconfig.json similarity index 100% rename from tests/interop/chrome-redis-safari/tsconfig.json rename to tests/interop/typescript-chrome-redis-safari/tsconfig.json diff --git a/tests/interop/chrome-redis-safari/verify_state.mjs b/tests/interop/typescript-chrome-redis-safari/verify_state.mjs similarity index 100% rename from tests/interop/chrome-redis-safari/verify_state.mjs rename to tests/interop/typescript-chrome-redis-safari/verify_state.mjs From 681737f741043ce42f62c80527dada8347538e23 Mon Sep 17 00:00:00 2001 From: Sagar Jaiswal Date: Tue, 1 Apr 2025 20:25:18 -0400 Subject: [PATCH 18/20] COmbination of interop tests --- tests/interop/README.md | 148 ------- .../python-chrome-redis-safari/README.md | 83 ---- .../create_state.py | 171 -------- .../requirements.txt | 5 - .../python-chrome-redis-safari/run_tests.sh | 214 ---------- .../verify_state.py | 247 ----------- .../interop/python-redis-typescript/README.md | 67 --- .../python-redis-typescript/create_state.py | 82 ---- .../python-redis-typescript/package-lock.json | 390 ------------------ .../python-redis-typescript/package.json | 30 -- .../python-redis-typescript/run_tests.sh | 89 ---- .../test_cross_language.py | 194 --------- .../python-redis-typescript/tsconfig.json | 13 - .../python-redis-typescript/verify_state.mjs | 140 ------- .../interop/python_tests/run_python_tests.py | 21 + .../python_tests/test_browser_state.py | 119 ++++++ tests/interop/run_all.sh | 88 ++-- tests/interop/setup.sh | 79 ++-- tests/interop/test_page/test.html | 31 ++ tests/interop/tsconfig.json | 14 + .../typescript-chrome-redis-safari/README.md | 73 ---- .../create_state.mjs | 163 -------- .../package-lock.json | 94 ----- .../package.json | 15 - .../run_tests.sh | 91 ---- .../tsconfig.json | 11 - .../verify_state.mjs | 260 ------------ .../interop/typescript-redis-python/README.md | 67 --- .../typescript-redis-python/create_state.mjs | 145 ------- .../typescript-redis-python/package-lock.json | 127 ------ .../typescript-redis-python/package.json | 17 - .../typescript-redis-python/run_tests.sh | 89 ---- .../typescript-redis-python/tsconfig.json | 13 - .../typescript-redis-python/verify_state.py | 122 ------ .../interop/typescript_tests/run_ts_tests.mjs | 29 ++ .../typescript_tests/test_browser_state.mjs | 128 ++++++ 36 files changed, 418 insertions(+), 3251 deletions(-) delete mode 100644 tests/interop/README.md delete mode 100644 tests/interop/python-chrome-redis-safari/README.md delete mode 100755 tests/interop/python-chrome-redis-safari/create_state.py delete mode 100644 tests/interop/python-chrome-redis-safari/requirements.txt delete mode 100755 tests/interop/python-chrome-redis-safari/run_tests.sh delete mode 100755 tests/interop/python-chrome-redis-safari/verify_state.py delete mode 100644 tests/interop/python-redis-typescript/README.md delete mode 100644 tests/interop/python-redis-typescript/create_state.py delete mode 100644 tests/interop/python-redis-typescript/package-lock.json delete mode 100644 tests/interop/python-redis-typescript/package.json delete mode 100755 tests/interop/python-redis-typescript/run_tests.sh delete mode 100644 tests/interop/python-redis-typescript/test_cross_language.py delete mode 100644 tests/interop/python-redis-typescript/tsconfig.json delete mode 100644 tests/interop/python-redis-typescript/verify_state.mjs create mode 100644 tests/interop/python_tests/run_python_tests.py create mode 100644 tests/interop/python_tests/test_browser_state.py create mode 100644 tests/interop/test_page/test.html create mode 100644 tests/interop/tsconfig.json delete mode 100644 tests/interop/typescript-chrome-redis-safari/README.md delete mode 100644 tests/interop/typescript-chrome-redis-safari/create_state.mjs delete mode 100644 tests/interop/typescript-chrome-redis-safari/package-lock.json delete mode 100644 tests/interop/typescript-chrome-redis-safari/package.json delete mode 100755 tests/interop/typescript-chrome-redis-safari/run_tests.sh delete mode 100644 tests/interop/typescript-chrome-redis-safari/tsconfig.json delete mode 100644 tests/interop/typescript-chrome-redis-safari/verify_state.mjs delete mode 100644 tests/interop/typescript-redis-python/README.md delete mode 100644 tests/interop/typescript-redis-python/create_state.mjs delete mode 100644 tests/interop/typescript-redis-python/package-lock.json delete mode 100644 tests/interop/typescript-redis-python/package.json delete mode 100755 tests/interop/typescript-redis-python/run_tests.sh delete mode 100644 tests/interop/typescript-redis-python/tsconfig.json delete mode 100644 tests/interop/typescript-redis-python/verify_state.py create mode 100644 tests/interop/typescript_tests/run_ts_tests.mjs create mode 100644 tests/interop/typescript_tests/test_browser_state.mjs diff --git a/tests/interop/README.md b/tests/interop/README.md deleted file mode 100644 index f8ae1fb..0000000 --- a/tests/interop/README.md +++ /dev/null @@ -1,148 +0,0 @@ -# BrowserState Interop Tests - -This directory contains interoperability tests for BrowserState: -1. Cross-language tests between Python and TypeScript implementations using Redis as the storage backend. -2. Cross-browser tests between different browsers (Chrome and Safari) using Redis as the storage backend. - -## Directory Structure - -``` -tests/interop/ -├── python-redis-typescript/ # Python -> Redis -> TypeScript tests -├── typescript-redis-python/ # TypeScript -> Redis -> Python tests -├── chrome-redis-safari/ # Chrome -> Redis -> Safari tests -├── setup.sh # Setup script for all interop tests -└── run_all.sh # Run all interop tests -``` - -## Prerequisites - -1. Redis server running locally (default port 6379) -2. Python 3.7+ -3. Node.js with npm -4. TypeScript and ts-node -5. Playwright for browser automation - -## Setup - -Run the setup script to prepare the test environments: - -```bash -./setup.sh -``` - -This will: -1. Create virtual environments for the test directories -2. Install all required dependencies: - - Python: boto3, google-cloud-storage, redis, playwright - - TypeScript: playwright, ts-node -3. Install the Python and TypeScript packages in development mode -4. Install and configure the Playwright browser automation tool - -## Running Tests - -To run all interop tests: - -```bash -./run_all.sh -``` - -This will: -1. Check if Redis is running -2. Verify that all required dependencies are installed -3. Run each test suite in sequence -4. Report success or failure for each test - -## Test Structure - -Each test directory contains: -- Scripts that create/verify browser state data -- A shell script (`run_tests.sh`) that orchestrates the test execution - -## Cross-Language Interoperability - -These tests verify that: - -1. Browser state created in Python can be correctly accessed in TypeScript -2. Browser state created in TypeScript can be correctly accessed in Python - -The tests focus on localStorage data persistence through Redis storage. - -## Cross-Browser Interoperability - -These tests verify that: - -1. Browser state created in one browser can be correctly accessed in another browser -2. Currently testing Chrome to Safari interoperability through Redis storage - -## GitHub Workflow Integration - -These tests are designed to be run in GitHub workflows. Example workflow step: - -```yaml -name: Interop Tests - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - interop-tests: - runs-on: ubuntu-latest - - services: - redis: - image: redis - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Setup Interop Test Environment - run: | - cd tests/interop - ./setup.sh - - - name: Run Interop Tests - run: | - cd tests/interop - ./run_all.sh -``` - -## Troubleshooting - -1. If Redis connection fails: - ```bash - redis-cli ping - ``` - -2. If dependencies are missing: - ```bash - cd tests/interop - ./setup.sh - ``` - -3. If Playwright browser installation fails: - ```bash - cd tests/interop/python-redis-typescript - source venv/bin/activate - python -m playwright install --help - ``` \ No newline at end of file diff --git a/tests/interop/python-chrome-redis-safari/README.md b/tests/interop/python-chrome-redis-safari/README.md deleted file mode 100644 index 77efe68..0000000 --- a/tests/interop/python-chrome-redis-safari/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# Python-Chrome to Safari Browser Interoperability Test - -This directory contains interoperability tests between Chrome and Safari browsers using Python with Redis as the storage backend. - -## Overview - -This test verifies that browser state (specifically localStorage data) can be: -1. Created in Chrome using the Python BrowserState library -2. Stored in Redis -3. Successfully restored in Safari - -## Prerequisites - -1. Redis server running locally (default port 6379) -2. Python 3.8+ with pip -3. Playwright for Python - -## Test Structure - -The test consists of two main scripts: - -1. `create_state.py` - Creates browser state in Chrome using Python and stores it in Redis -2. `verify_state.py` - Loads the Chrome-created state in Safari and verifies it - -## Running the Test - -To run the entire test suite: - -```bash -./run_tests.sh -``` - -This will: -- Check if Redis is running -- Set up a Python virtual environment with necessary dependencies -- Run the Chrome state creation script -- Run the Safari verification script - -To run individual steps (after setting up the environment): - -```bash -# Create state in Chrome -python create_state.py - -# Verify state in Safari -python verify_state.py -``` - -## Test Details - -### Chrome State Creation (`create_state.py`) - -1. Initializes a BrowserState instance with Redis storage -2. Mounts a new session for the browser state -3. Launches a Chrome browser with the mounted state using Playwright -4. Adds test data to localStorage: - - Multiple note items - - Browser metadata (user agent, timestamp, creator information) -5. Unmounts the session, saving state to Redis - -### Safari Verification (`verify_state.py`) - -1. Initializes a BrowserState instance with the same Redis configuration -2. Lists available sessions to find the Chrome-created session -3. Mounts the Chrome-created session -4. Launches a Safari browser with the mounted state using Playwright -5. Verifies that the localStorage data created in Chrome is accessible in Safari: - - Notes are present and contain the expected content - - Browser metadata shows it was created in Chrome by Python -6. Adds Safari verification metadata to localStorage -7. Unmounts the session - -## Troubleshooting - -If the test fails, check: - -1. Redis is running: `redis-cli ping` -2. Playwright browsers are installed: `python -m playwright install chromium webkit` -3. The BrowserState Python package is installed correctly - -## Notes - -This test demonstrates cross-browser and cross-language interoperability of the BrowserState library, showing how state can be seamlessly transferred between different browser environments. \ No newline at end of file diff --git a/tests/interop/python-chrome-redis-safari/create_state.py b/tests/interop/python-chrome-redis-safari/create_state.py deleted file mode 100755 index 3b064cd..0000000 --- a/tests/interop/python-chrome-redis-safari/create_state.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python3 -""" -Python to Safari Browser Interoperability Test - -This script creates browser state in Chrome and stores it in Redis, -which can then be loaded by Safari to test cross-browser interoperability. -""" - -import os -import json -import sys -import asyncio -from pathlib import Path -from playwright.async_api import async_playwright - -# Try different import patterns to handle potential module structure issues -try: - from browserstate import BrowserState, BrowserStateOptions, RedisStorage - print("✅ Successfully imported browserstate from top-level package") -except ImportError: - try: - # Try importing from submodules - from browserstate.browser_state import BrowserState, BrowserStateOptions - from browserstate.storage.redis_storage import RedisStorage - print("✅ Successfully imported browserstate from submodules") - except ImportError as e: - print(f"❌ Failed to import browserstate: {e}") - print("System path:", sys.path) - sys.exit(1) - -# Test constants -SESSION_ID = "python_chrome_to_safari_test" -USER_ID = "browser_interop_user" - -# Debug mode - set to True to see browser UI -DEBUG = False - -# Redis configuration -REDIS_CONFIG = { - "redis_url": "redis://localhost:6379/0", - "key_prefix": "browserstate" -} - -# Path to the test HTML file -TEST_HTML_PATH = Path(__file__).parent.parent.parent.parent / "typescript" / "examples" / "shared" / "test.html" -TEST_URL = f"file://{TEST_HTML_PATH.absolute()}" - -def fail_test(message): - """Fail the test with a clear error message.""" - print(f"\n❌ TEST FAILED: {message}") - sys.exit(1) - -async def create_browser_state(): - """Create browser state in Chrome and store it in Redis.""" - print("🚀 Starting Chrome State Creation using Python\n") - - try: - # Initialize BrowserState with Redis storage - print("🔧 Creating BrowserState with Redis storage...") - options = BrowserStateOptions( - user_id=USER_ID, - redis_options=REDIS_CONFIG - ) - browser_state = BrowserState(options) - - # List existing sessions - print("\n📋 Listing existing sessions...") - sessions = browser_state.list_sessions() - print(f"Found {len(sessions)} session(s): {', '.join(sessions) if sessions else 'None'}") - - # Mount the session - print(f"\n📥 Mounting session: {SESSION_ID}") - mount_result = browser_state.mount_session(SESSION_ID) - user_data_dir = mount_result["path"] - print(f"📂 Mounted at: {user_data_dir}") - - # Launch Chrome browser with the mounted state - print("\n🌐 Launching Chrome browser with Playwright...") - async with async_playwright() as p: - browser = await p.chromium.launch_persistent_context( - user_data_dir=user_data_dir, - headless=not DEBUG - ) - - try: - # Create a new page - page = await browser.new_page() - - # Navigate to the test HTML page - print(f"\n📄 Loading test page: {TEST_URL}") - await page.goto(TEST_URL) - print("✅ Test page loaded") - - # Clear existing localStorage to start fresh - await page.evaluate("() => { localStorage.clear(); }") - - # Wait for the page to load completely - await page.wait_for_timeout(1000) - - # Add Chrome-specific test data to localStorage - print("\n📝 Adding Chrome-specific test data to localStorage...") - - # Add notes to localStorage - test_notes = [ - "Python-Chrome created note 1", - "Python-Chrome created note 2", - "Python-Chrome created note 3" - ] - - for note in test_notes: - await page.fill("#noteInput", note) - await page.click("button:text('Add Note')") - await page.wait_for_timeout(500) # Wait for animation - - # Add browser metadata to localStorage - await page.evaluate("""() => { - localStorage.setItem('browserMetadata', JSON.stringify({ - browser: 'Chrome', - createdBy: 'Python', - timestamp: new Date().toISOString(), - userAgent: navigator.userAgent - })); - }""") - - # Verify notes were added - notes_count = await page.evaluate("""() => { - return JSON.parse(localStorage.getItem('notes') || '[]').length; - }""") - - # Verify notes were added successfully - if notes_count != len(test_notes): - fail_test(f"Expected {len(test_notes)} notes, but found {notes_count}") - - print(f"✅ Added {notes_count} notes") - - # Get the notes data for verification - notes_data = await page.evaluate("""() => { - return localStorage.getItem('notes'); - }""") - - # Get browser metadata - metadata_json = await page.evaluate("""() => { - return localStorage.getItem('browserMetadata'); - }""") - - # Verify data is not empty - if not notes_data or not metadata_json: - fail_test("Test data is empty") - - print(f"📝 Notes data: {notes_data}") - print(f"📝 Browser metadata: {metadata_json}") - - # Wait to ensure data is properly saved - await page.wait_for_timeout(1000) - - finally: - await browser.close() - - # Unmount the session - print("\n🔒 Unmounting session...") - browser_state.unmount_session() - print("✅ Session unmounted") - - print("\n✨ Chrome state creation complete!") - print("Now run verify_state.py to verify the state in Safari") - - except Exception as e: - fail_test(f"Error during Chrome state creation: {str(e)}") - -if __name__ == "__main__": - asyncio.run(create_browser_state()) \ No newline at end of file diff --git a/tests/interop/python-chrome-redis-safari/requirements.txt b/tests/interop/python-chrome-redis-safari/requirements.txt deleted file mode 100644 index 4d0981c..0000000 --- a/tests/interop/python-chrome-redis-safari/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -playwright>=1.35.0 -redis>=4.5.0 -boto3>=1.26.0 -google-cloud-storage>=2.7.0 -# Local browserstate package should be installed with pip install -e /path/to/browserstate/python \ No newline at end of file diff --git a/tests/interop/python-chrome-redis-safari/run_tests.sh b/tests/interop/python-chrome-redis-safari/run_tests.sh deleted file mode 100755 index cc3040b..0000000 --- a/tests/interop/python-chrome-redis-safari/run_tests.sh +++ /dev/null @@ -1,214 +0,0 @@ -#!/bin/bash -# Shell script to run Python-Chrome to Safari browser interop tests - -set -e # Exit immediately if a command exits with a non-zero status - -# Colors for console output -GREEN='\033[0;32m' -BLUE='\033[0;34m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Get directory of this script -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" -PYTHON_DIR="${SCRIPT_DIR}/../../../python" - -# Print test header -echo -e "${BLUE}========================================================${NC}" -echo -e "${BLUE} Python-Chrome to Safari via Redis Interop Test ${NC}" -echo -e "${BLUE}========================================================${NC}" - -# Function to check if Redis is running -check_redis() { - echo -e "\n${YELLOW}Checking if Redis is running...${NC}" - if command -v redis-cli >/dev/null 2>&1 ; then - if redis-cli ping > /dev/null 2>&1; then - echo -e "${GREEN}✓ Redis is running${NC}" - else - echo -e "${RED}✗ Redis is not running${NC}" - echo -e "${YELLOW}Please start Redis:${NC}" - echo -e " redis-server" - exit 1 - fi - else - echo -e "${RED}✗ redis-cli not found${NC}" - echo -e "${YELLOW}Please install Redis:${NC}" - echo -e " brew install redis" - exit 1 - fi -} - -# Function to check and set up Python environment -setup_python() { - echo -e "\n${YELLOW}Checking Python environment...${NC}" - - # Create virtual environment if it doesn't exist - if [ ! -d "${SCRIPT_DIR}/venv" ]; then - echo -e "${YELLOW}Creating Python virtual environment...${NC}" - python3 -m venv "${SCRIPT_DIR}/venv" - echo -e "${GREEN}✓ Virtual environment created${NC}" - fi - - # Activate virtual environment - source "${SCRIPT_DIR}/venv/bin/activate" - echo -e "${GREEN}✓ Virtual environment activated${NC}" - - # Install required packages - echo -e "\n${YELLOW}Installing required Python packages...${NC}" - - # Install base dependencies first - pip install --upgrade pip - - # Install dependencies from requirements.txt - if [ -f "${SCRIPT_DIR}/requirements.txt" ]; then - echo -e "${YELLOW}Installing dependencies from requirements.txt...${NC}" - pip install -r "${SCRIPT_DIR}/requirements.txt" - else - echo -e "${YELLOW}No requirements.txt found, installing required packages individually...${NC}" - pip install playwright redis boto3 google-cloud-storage - fi - - # Install local browserstate package if in development mode - if [ -d "${PYTHON_DIR}" ]; then - echo -e "${YELLOW}Installing local browserstate package from: ${PYTHON_DIR}${NC}" - pip install -e "${PYTHON_DIR}" - if [ $? -ne 0 ]; then - echo -e "${RED}✗ Failed to install local browserstate package${NC}" - echo -e "${YELLOW}Trying to install from PyPI instead...${NC}" - pip install browserstate - else - echo -e "${GREEN}✓ Local browserstate package installed${NC}" - fi - else - echo -e "${YELLOW}No local browserstate package found, installing from PyPI...${NC}" - pip install browserstate - fi - - # Check if browserstate was successfully installed - if ! python -c "import browserstate" &> /dev/null; then - echo -e "${RED}✗ Failed to import browserstate module${NC}" - echo -e "${YELLOW}Debugging module import issue...${NC}" - # Show installed packages - echo -e "${YELLOW}Installed packages:${NC}" - pip list | grep browserstate - - # Check the Python path - echo -e "${YELLOW}Python path:${NC}" - python -c "import sys; print(sys.path)" - - # Check if browserstate package structure looks correct - echo -e "${YELLOW}Checking browserstate package structure:${NC}" - site_packages=$(python -c "import site; print(site.getsitepackages()[0])") - if [ -d "$site_packages/browserstate" ]; then - echo -e "${GREEN}✓ Browserstate package directory exists${NC}" - ls -la "$site_packages/browserstate" - - # Check for critical files - if [ -f "$site_packages/browserstate/__init__.py" ]; then - echo -e "${GREEN}✓ __init__.py file exists${NC}" - else - echo -e "${RED}✗ Missing __init__.py file${NC}" - fi - - # Check if the module has the expected structure - if [ -f "$site_packages/browserstate/browser_state.py" ]; then - echo -e "${GREEN}✓ browser_state.py exists${NC}" - else - echo -e "${RED}✗ Missing browser_state.py${NC}" - fi - - # Check if storage directory exists - if [ -d "$site_packages/browserstate/storage" ]; then - echo -e "${GREEN}✓ storage directory exists${NC}" - ls -la "$site_packages/browserstate/storage" - else - echo -e "${RED}✗ Missing storage directory${NC}" - fi - else - echo -e "${RED}✗ Browserstate package directory not found${NC}" - # Look for other browserstate-related files - echo -e "${YELLOW}Looking for browserstate files in site-packages:${NC}" - find "$site_packages" -name "*browserstate*" - fi - - # Continue anyway, as the package might still work - echo -e "${YELLOW}Will attempt to continue despite import issue...${NC}" - else - echo -e "${GREEN}✓ Browserstate module imported successfully${NC}" - fi - - # Install Playwright browsers - echo -e "\n${YELLOW}Installing Playwright browsers...${NC}" - python -m playwright install chromium webkit - - echo -e "${GREEN}✓ All Python dependencies installed${NC}" - - # Print browserstate version for debugging - echo -e "\n${YELLOW}Installed browserstate version:${NC}" - python -c " -import sys -try: - import browserstate - if hasattr(browserstate, '__version__'): - print(f'Version: {browserstate.__version__}') - else: - print('Version information not available in package') -except ImportError as e: - print(f'Import error: {e}') -except Exception as e: - print(f'Unexpected error: {e}') -" - - # Check if all required dependencies are installed - echo -e "\n${YELLOW}Checking required dependencies...${NC}" - for pkg in boto3 redis google.cloud playwright browserstate; do - if ! python -c "import $pkg" &> /dev/null; then - echo -e "${RED}✗ Python package '$pkg' is not installed${NC}" - echo -e "${YELLOW}Installing missing package: $pkg${NC}" - pip install $pkg - - # Verify installation - if ! python -c "import $pkg" &> /dev/null; then - echo -e "${RED}✗ Failed to install $pkg. This might cause issues.${NC}" - else - echo -e "${GREEN}✓ Successfully installed $pkg${NC}" - fi - else - echo -e "${GREEN}✓ Package $pkg is installed${NC}" - fi - done -} - -# Check prerequisites -check_redis -setup_python - -# Run Chrome state creation -echo -e "\n${YELLOW}Step 1: Creating browser state in Chrome using Python...${NC}" -python "${SCRIPT_DIR}/create_state.py" -if [ $? -ne 0 ]; then - echo -e "${RED}✗ Chrome state creation failed${NC}" - exit 1 -fi -echo -e "${GREEN}✓ Chrome state creation completed${NC}" - -# Run Safari state verification -echo -e "\n${YELLOW}Step 2: Verifying browser state in Safari using Python...${NC}" -python "${SCRIPT_DIR}/verify_state.py" -if [ $? -ne 0 ]; then - echo -e "${RED}✗ Safari state verification failed${NC}" - exit 1 -fi -echo -e "${GREEN}✓ Safari state verification completed${NC}" - -# Test complete -echo -e "\n${GREEN}========================================================${NC}" -echo -e "${GREEN} 🎉 Python-Chrome to Safari interop test completed successfully! ${NC}" -echo -e "${GREEN}========================================================${NC}" - -# Deactivate virtual environment -deactivate -echo -e "${YELLOW}Python virtual environment deactivated${NC}" - -exit 0 \ No newline at end of file diff --git a/tests/interop/python-chrome-redis-safari/verify_state.py b/tests/interop/python-chrome-redis-safari/verify_state.py deleted file mode 100755 index 5c0cc6e..0000000 --- a/tests/interop/python-chrome-redis-safari/verify_state.py +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env python3 -""" -Python to Safari Browser Interoperability Test - Verification - -This script verifies that browser state created in Chrome by Python -can be loaded and used correctly in Safari. -""" - -import os -import json -import sys -import asyncio -from pathlib import Path -from playwright.async_api import async_playwright - -# Try different import patterns to handle potential module structure issues -try: - from browserstate import BrowserState, BrowserStateOptions, RedisStorage - print("✅ Successfully imported browserstate from top-level package") -except ImportError: - try: - # Try importing from submodules - from browserstate.browser_state import BrowserState, BrowserStateOptions - from browserstate.storage.redis_storage import RedisStorage - print("✅ Successfully imported browserstate from submodules") - except ImportError as e: - print(f"❌ Failed to import browserstate: {e}") - print("System path:", sys.path) - sys.exit(1) - -# Test constants - must match create_state.py -SESSION_ID = "python_chrome_to_safari_test" -USER_ID = "browser_interop_user" - -# Debug mode - set to True to see browser UI -DEBUG = False - -# Redis configuration -REDIS_CONFIG = { - "redis_url": "redis://localhost:6379/0", - "key_prefix": "browserstate" -} - -# Path to the test HTML file -TEST_HTML_PATH = Path(__file__).parent.parent.parent.parent / "typescript" / "examples" / "shared" / "test.html" -TEST_URL = f"file://{TEST_HTML_PATH.absolute()}" - -def fail_test(message): - """Fail the test with a clear error message.""" - print(f"\n❌ TEST FAILED: {message}") - sys.exit(1) - -async def verify_browser_state(): - """Verify browser state in Safari.""" - print("\n🔍 Verifying Chrome-created browser state in Safari using Python\n") - - try: - # Initialize BrowserState with Redis storage - print("🔧 Creating BrowserState with Redis storage...") - options = BrowserStateOptions( - user_id=USER_ID, - redis_options=REDIS_CONFIG - ) - browser_state = BrowserState(options) - - # List available sessions - print("\n📋 Listing available sessions...") - sessions = browser_state.list_sessions() - print(f"Found {len(sessions)} session(s): {', '.join(sessions) if sessions else 'None'}") - - if SESSION_ID not in sessions: - fail_test(f"Session '{SESSION_ID}' not found") - - # Mount the session - print(f"\n📥 Mounting session: {SESSION_ID}") - mount_result = browser_state.mount_session(SESSION_ID) - user_data_dir = mount_result["path"] - print(f"📂 Mounted at: {user_data_dir}") - - # Create an HTML file that will help initialize Safari's localStorage - # This is necessary because Safari may not directly read Chrome's localStorage format - init_script_path = os.path.join(user_data_dir, "init-storage.html") - print("\n📝 Creating initialization script for Safari...") - - # Create sample notes data (in case we can't retrieve from Chrome directly) - sample_notes_data = json.dumps([ - {"text":"Python-Chrome created note 1","timestamp":"2025-04-01T19:26:34.473Z"}, - {"text":"Python-Chrome created note 2","timestamp":"2025-04-01T19:26:35.022Z"}, - {"text":"Python-Chrome created note 3","timestamp":"2025-04-01T19:26:35.574Z"} - ]) - - sample_metadata = json.dumps({ - "browser":"Chrome", - "createdBy":"Python", - "timestamp":"2025-04-01T19:26:36.081Z", - "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/112.0.5615.49 Safari/537.36" - }) - - # Create an HTML file that will initialize localStorage - with open(init_script_path, 'w') as f: - f.write(f""" - - - - Initialize Storage - - -

Initializing cross-browser storage...

- - - - """) - - print(f"✅ Created init-storage.html in user data directory for Safari") - - # Create the file:// URL for the init script - init_script_url = f"file://{init_script_path}" - - # Launch Safari browser with the mounted state - print("\n🌐 Launching Safari browser with Playwright...") - async with async_playwright() as p: - browser = await p.webkit.launch_persistent_context( - user_data_dir=user_data_dir, - headless=not DEBUG - ) - - try: - # Create a new page - page = await browser.new_page() - - # First navigate to the init script to set up localStorage - print(f"\n📄 Loading initialization page in Safari: {init_script_url}") - await page.goto(init_script_url) - await page.wait_for_timeout(1000) - - # The init script will automatically redirect to the test page - print(f"📄 Waiting for test page to load: {TEST_URL}") - await page.wait_for_url(TEST_URL) - print("✅ Test page loaded in Safari") - - # Retrieve notes from localStorage - notes_json = await page.evaluate("""() => { - return localStorage.getItem('notes'); - }""") - - # Retrieve browser metadata - metadata_json = await page.evaluate("""() => { - return localStorage.getItem('browserMetadata'); - }""") - - if not notes_json: - # Try to debug by looking at all localStorage items - storage_items = await page.evaluate("""() => { - const items = {}; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - items[key] = localStorage.getItem(key); - } - return items; - }""") - print(f"Available localStorage items: {json.dumps(storage_items)}") - fail_test("No notes found in localStorage") - - if not metadata_json: - fail_test("No browser metadata found in localStorage") - - # Parse JSON data - notes = json.loads(notes_json) - metadata = json.loads(metadata_json) - - # Verify notes - print(f"\n📝 Found {len(notes)} notes in Safari:") - for note in notes: - print(f" - {note['text']} (created at: {note['timestamp']})") - - # Verify Python-Chrome-created notes - python_chrome_notes = [note for note in notes if note['text'].startswith('Python-Chrome created')] - if not python_chrome_notes: - fail_test('No Python-Chrome-created notes found') - - print(f"✅ Found {len(python_chrome_notes)} Python-Chrome-created notes in Safari") - - # Verify browser metadata - print('\n📊 Browser metadata:') - print(f" - Original browser: {metadata.get('browser', 'Unknown')}") - print(f" - Created by: {metadata.get('createdBy', 'Unknown')}") - print(f" - Creation timestamp: {metadata.get('timestamp', 'Unknown')}") - - if metadata.get('browser') != 'Chrome': - fail_test(f"Expected metadata.browser to be 'Chrome', but found '{metadata.get('browser', 'Unknown')}'") - - if metadata.get('createdBy') != 'Python': - fail_test(f"Expected metadata.createdBy to be 'Python', but found '{metadata.get('createdBy', 'Unknown')}'") - - # Add Safari verification metadata - await page.evaluate("""() => { - localStorage.setItem('safariVerification', JSON.stringify({ - verifiedIn: 'Safari', - verifiedBy: 'Python', - timestamp: new Date().toISOString(), - userAgent: navigator.userAgent - })); - }""") - - # Wait to ensure data is properly saved - await page.wait_for_timeout(1000) - - # Verify the Safari verification metadata was saved - safari_verification_json = await page.evaluate("""() => { - return localStorage.getItem('safariVerification'); - }""") - - if not safari_verification_json: - fail_test('Failed to save Safari verification metadata') - - safari_verification = json.loads(safari_verification_json) - print('\n✅ Added Safari verification metadata:') - print(f" - Verified in: {safari_verification.get('verifiedIn', 'Unknown')}") - print(f" - Verified by: {safari_verification.get('verifiedBy', 'Unknown')}") - print(f" - Verification timestamp: {safari_verification.get('timestamp', 'Unknown')}") - - finally: - await browser.close() - - # Unmount the session - print("\n🔒 Unmounting session...") - browser_state.unmount_session() - print("✅ Session unmounted") - - print("\n✨ Safari verification complete!") - print("Python-Chrome to Safari interoperability test passed!") - - except Exception as e: - fail_test(f"Error during Safari verification: {str(e)}") - -if __name__ == "__main__": - asyncio.run(verify_browser_state()) \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/README.md b/tests/interop/python-redis-typescript/README.md deleted file mode 100644 index c717d2c..0000000 --- a/tests/interop/python-redis-typescript/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Python -> Redis -> TypeScript Interoperability Test - -This test verifies the cross-language interoperability between Python and TypeScript implementations of BrowserState using Redis as the storage backend. - -## Overview - -The test performs the following: - -1. Uses Python to create browser state data (localStorage entries) using Playwright -2. Stores the state in Redis -3. Uses TypeScript to load the state from Redis using Playwright -4. Verifies that the state created in Python can be correctly read in TypeScript - -## Requirements - -- Redis server running on localhost:6379 -- Python 3.6+ with pip -- Node.js with npm -- BrowserState Python package installed -- BrowserState TypeScript package installed - -## Files - -- `test_cross_language.py`: Python script that creates state data and saves it to Redis -- `verify_state.ts`: TypeScript script that loads state from Redis and verifies it -- `run_tests.sh`: Shell script that installs dependencies and runs both tests in sequence - -## Running the Tests - -To run the tests, execute: - -```bash -./run_tests.sh -``` - -The script will: -1. Check if Redis is running -2. Install required dependencies (Playwright for both Python and TypeScript) -3. Run the Python test to create state -4. Run the TypeScript test to verify state - -## How it Works - -### Python Side - -The Python script: -1. Initializes a BrowserState instance with Redis storage -2. Mounts a session with a specific ID -3. Uses Playwright to load a test HTML page -4. Adds notes to localStorage in the browser -5. Unmounts the session, which saves the state to Redis - -### TypeScript Side - -The TypeScript script: -1. Initializes a BrowserState instance with the same Redis configuration -2. Mounts the same session ID -3. Uses Playwright to load the same test HTML page -4. Reads the localStorage data created by Python -5. Verifies that the data created by Python exists and is correct - -## Troubleshooting - -- If Redis is not running, start it with `brew services start redis` (macOS) or `redis-server` (Linux) -- If dependencies are not installed correctly, you can install them manually: - - For Python: `pip install playwright && python -m playwright install chromium` - - For TypeScript: `npm install playwright ts-node` \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/create_state.py b/tests/interop/python-redis-typescript/create_state.py deleted file mode 100644 index 7e97790..0000000 --- a/tests/interop/python-redis-typescript/create_state.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -""" -Example demonstrating Python to TypeScript interop with Redis storage. -This script creates a browser state in Python and stores it in Redis, -which can then be read by the TypeScript implementation. -""" - -import os -import json -import tempfile -import shutil -from pathlib import Path -from browserstate import BrowserState, RedisStorage - -# Redis configuration matching TypeScript example -REDIS_CONFIG = { - "host": "localhost", - "port": 6379, - "password": None, - "db": 0, - "key_prefix": "browserstate:", - "ttl": 604800 # 7 days -} - -def create_test_data(base_dir: str) -> None: - """Create test data in the specified directory.""" - # Create a test file - test_file = Path(base_dir) / "test.txt" - test_file.write_text("Test data from Python implementation") - - # Create a subdirectory with nested files - subdir = Path(base_dir) / "subdir" - subdir.mkdir(exist_ok=True) - (subdir / "nested.txt").write_text("Nested file from Python") - - # Create a JSON file with metadata - metadata = { - "created_by": "python", - "timestamp": "2024-04-01T00:00:00Z", - "version": "1.0.0" - } - (base_dir / "metadata.json").write_text(json.dumps(metadata, indent=2)) - -def main(): - print("🚀 Starting Python to TypeScript Redis Interop Demo\n") - - # Create temporary directory for test data - with tempfile.TemporaryDirectory() as temp_dir: - print(f"📂 Created temporary directory: {temp_dir}") - - # Create test data - create_test_data(Path(temp_dir)) - print("📝 Created test files") - - # Initialize browser state with Redis storage - print("\n🔧 Initializing BrowserState with Redis storage...") - browser_state = BrowserState( - user_id="interop_test_user", - storage_type="redis", - redis_options=REDIS_CONFIG - ) - - # Upload the test data - print("\n📤 Uploading test data to Redis...") - session_id = "python_created_session" - browser_state.upload(session_id, temp_dir) - - # Verify the upload - print("\n✅ Verification:") - print(f" - User ID: interop_test_user") - print(f" - Session ID: {session_id}") - print(f" - Key prefix: {REDIS_CONFIG['key_prefix']}") - - # List sessions to verify - sessions = browser_state.list_sessions() - print(f"\n📋 Available sessions: {sessions}") - - print("\n✨ Python state creation complete!") - print("You can now run the TypeScript example to verify the state") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/package-lock.json b/tests/interop/python-redis-typescript/package-lock.json deleted file mode 100644 index 548be8b..0000000 --- a/tests/interop/python-redis-typescript/package-lock.json +++ /dev/null @@ -1,390 +0,0 @@ -{ - "name": "python-redis-typescript", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "python-redis-typescript", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "acorn": "^8.14.1", - "acorn-walk": "^8.3.4", - "arg": "^4.1.3", - "create-require": "^1.1.1", - "diff": "^4.0.2", - "ioredis": "^5.6.0", - "make-error": "^1.3.6", - "playwright": "^1.51.1", - "playwright-core": "^1.51.1", - "ts-node": "^10.9.2", - "typescript": "^5.8.2", - "undici-types": "^6.20.0", - "v8-compile-cache-lib": "^3.0.1", - "yn": "^3.1.1" - }, - "devDependencies": {} - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", - "license": "MIT" - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", - "license": "MIT", - "peer": true, - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/node/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "license": "MIT", - "peer": true - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "license": "MIT" - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/ioredis": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz", - "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "^1.1.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT" - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "license": "ISC" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/playwright": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", - "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.51.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", - "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "license": "MIT" - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - } - } -} diff --git a/tests/interop/python-redis-typescript/package.json b/tests/interop/python-redis-typescript/package.json deleted file mode 100644 index 9cea997..0000000 --- a/tests/interop/python-redis-typescript/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "python-redis-typescript", - "version": "1.0.0", - "main": "index.js", - "type": "module", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "description": "This test verifies the cross-language interoperability between Python and TypeScript implementations of BrowserState using Redis as the storage backend.", - "dependencies": { - "acorn": "^8.14.1", - "acorn-walk": "^8.3.4", - "arg": "^4.1.3", - "create-require": "^1.1.1", - "diff": "^4.0.2", - "ioredis": "^5.6.0", - "make-error": "^1.3.6", - "playwright": "^1.51.1", - "playwright-core": "^1.51.1", - "ts-node": "^10.9.2", - "typescript": "^5.8.2", - "undici-types": "^6.20.0", - "v8-compile-cache-lib": "^3.0.1", - "yn": "^3.1.1" - }, - "devDependencies": {} -} diff --git a/tests/interop/python-redis-typescript/run_tests.sh b/tests/interop/python-redis-typescript/run_tests.sh deleted file mode 100755 index 0557fbf..0000000 --- a/tests/interop/python-redis-typescript/run_tests.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -# Exit on error -set -e - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Function to print section headers -print_header() { - echo -e "\n${BLUE}=== $1 ===${NC}\n" -} - -# Function to check if Redis is running -check_redis() { - print_header "Checking Redis Connection" - if ! redis-cli ping > /dev/null 2>&1; then - echo -e "${RED}❌ Redis is not running. Please start Redis first.${NC}" - exit 1 - fi - echo -e "${GREEN}✅ Redis is running${NC}" -} - -# Activate the virtual environment -print_header "Activating Python Environment" -if [ -f "venv/bin/activate" ]; then - source venv/bin/activate - echo -e "${GREEN}✅ Virtual environment activated${NC}" -else - echo -e "${RED}❌ Virtual environment not found. Please run the setup script first: cd .. && ./setup.sh${NC}" - exit 1 -fi - -# Function to check and install required dependencies -check_dependencies() { - print_header "Checking Required Dependencies" - - # Check Python dependencies - for pkg in boto3 redis google.cloud playwright browserstate; do - if ! python -c "import $pkg" &> /dev/null; then - echo -e "${RED}❌ Python package '$pkg' is not installed.${NC}" - echo -e "${RED}Please run the setup script first: cd .. && ./setup.sh${NC}" - exit 1 - fi - done - - # Check TypeScript dependencies using package.json - if [ ! -f "node_modules/playwright/package.json" ] || [ ! -f "node_modules/ts-node/package.json" ]; then - echo -e "${RED}❌ TypeScript dependencies are not installed.${NC}" - echo -e "${RED}Please run the setup script first: cd .. && ./setup.sh${NC}" - exit 1 - fi - - # Ensure ioredis is installed - if [ ! -f "node_modules/ioredis/package.json" ]; then - echo -e "${BLUE}Installing ioredis...${NC}" - npm install ioredis - fi - - echo -e "${GREEN}✅ All dependencies installed${NC}" -} - -# Main execution -echo -e "${BLUE}🚀 Starting Python -> Redis -> TypeScript Interop Test${NC}" - -# Check prerequisites -check_redis -check_dependencies - -# Run Python test -print_header "Running Python Test" -if ! python3 test_cross_language.py; then - echo -e "${RED}❌ Python test failed${NC}" - exit 1 -fi -echo -e "${GREEN}✅ Python test completed successfully${NC}" - -# Run TypeScript verification -print_header "Running TypeScript Verification" -if ! node verify_state.mjs; then - echo -e "${RED}❌ TypeScript verification failed${NC}" - exit 1 -fi -echo -e "${GREEN}✅ TypeScript verification completed successfully${NC}" - -echo -e "\n${GREEN}✨ All tests completed successfully!${NC}" \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/test_cross_language.py b/tests/interop/python-redis-typescript/test_cross_language.py deleted file mode 100644 index d90e150..0000000 --- a/tests/interop/python-redis-typescript/test_cross_language.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env python3 -""" -Cross-language interop test using Playwright to verify browser state persistence -between Python and TypeScript implementations. -""" - -import os -import json -import asyncio -import sys -from pathlib import Path -from playwright.async_api import async_playwright -from browserstate import BrowserState, BrowserStateOptions, RedisStorage - -# Redis configuration for Python -REDIS_URL = "redis://localhost:6379/0" -REDIS_KEY_PREFIX = "browserstate" - -# Test session ID - must be the same across Python and TypeScript -SESSION_ID = "cross_language_test" -USER_ID = "interop_test_user" - -# Path to the test HTML file -TEST_HTML_PATH = Path(__file__).parent.parent.parent.parent / "typescript" / "examples" / "shared" / "test.html" -TEST_URL = f"file://{TEST_HTML_PATH.absolute()}" - -# Debug mode - set to True to see browser UI during tests -DEBUG = False - -def fail_test(message): - """Fail the test with a clear error message.""" - print(f"\n❌ TEST FAILED: {message}") - sys.exit(1) - -async def create_test_data(browser_state: BrowserState, session_id: str) -> None: - """Create test data using Playwright and store it in Redis.""" - print(f"\n🔧 Creating test data in session: {session_id}") - - # Mount the session - state = browser_state.mount_session(session_id) - print(f"📂 Mounted session at: {state['path']}") - - async with async_playwright() as p: - # Launch browser with the mounted state - browser = await p.chromium.launch_persistent_context( - user_data_dir=state["path"], - headless=not DEBUG - ) - - try: - # Create a new page - page = await browser.new_page() - - # Navigate to the test HTML page - print(f"📄 Loading test page: {TEST_URL}") - await page.goto(TEST_URL) - - # Clear existing localStorage - await page.evaluate("""() => { - localStorage.clear(); - }""") - - # Add some test notes - test_notes = [ - "Python created note 1", - "Python created note 2", - "Python created note 3" - ] - - for note in test_notes: - await page.fill("#noteInput", note) - await page.click("button:text('Add Note')") - await page.wait_for_timeout(500) # Wait for animation - - # Verify notes were added - notes_count = await page.evaluate("""() => { - return JSON.parse(localStorage.getItem('notes') || '[]').length; - }""") - print(f"✅ Added {notes_count} notes") - - # Assert that notes were actually added - if notes_count != len(test_notes): - fail_test(f"Expected {len(test_notes)} notes, but found {notes_count}") - - # Get the notes data for verification - notes_data = await page.evaluate("""() => { - return localStorage.getItem('notes'); - }""") - print(f"📝 Notes data: {notes_data}") - - # Verify notes data is not empty - if not notes_data: - fail_test("Notes data is empty") - - # Wait to ensure data is properly saved - await page.wait_for_timeout(1000) - - finally: - await browser.close() - - # Unmount the session to save changes - browser_state.unmount_session() - print("💾 Saved session state to Redis") - -async def verify_test_data(browser_state: BrowserState, session_id: str) -> None: - """Verify that the test data can be read by TypeScript implementation.""" - print(f"\n🔍 Verifying test data in session: {session_id}") - - # Mount the session - state = browser_state.mount_session(session_id) - print(f"📂 Mounted session at: {state['path']}") - - async with async_playwright() as p: - # Launch browser with the mounted state - browser = await p.chromium.launch_persistent_context( - user_data_dir=state["path"], - headless=not DEBUG - ) - - try: - # Create a new page - page = await browser.new_page() - - # Navigate to the test HTML page - using the EXACT same URL - print(f"📄 Loading test page: {TEST_URL}") - await page.goto(TEST_URL) - - # Wait for the page to load completely - await page.wait_for_timeout(1000) - - # Get the notes data - notes_data = await page.evaluate("""() => { - return localStorage.getItem('notes'); - }""") - - if notes_data: - notes = json.loads(notes_data) - print(f"📝 Found {len(notes)} notes:") - for note in notes: - print(f" - {note['text']} ({note['timestamp']})") - - # Verify that we have Python notes - python_notes = [note for note in notes if note['text'].startswith('Python created note')] - if not python_notes: - fail_test("No Python-created notes found in localStorage") - - print(f"✅ Found {len(python_notes)} Python-created notes") - else: - # Try to debug by looking at all localStorage items - storage_items = await page.evaluate("""() => { - const items = {}; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - items[key] = localStorage.getItem(key); - } - return items; - }""") - print(f"Available localStorage items: {storage_items}") - fail_test("No notes found in localStorage") - - finally: - await browser.close() - - # Unmount the session - browser_state.unmount_session() - print("✅ Verification complete") - -async def main(): - """Main function to run the cross-language interop test.""" - print("🚀 Starting Python -> Redis -> TypeScript Interop Test\n") - - # Create a Redis storage provider with the correct URL format - redis_options = { - "redis_url": REDIS_URL, - "key_prefix": REDIS_KEY_PREFIX - } - - # Initialize browser state with Redis storage - options = BrowserStateOptions( - user_id=USER_ID, - redis_options=redis_options - ) - browser_state = BrowserState(options) - - # Create test data with Python - await create_test_data(browser_state, SESSION_ID) - - # Verify the data can be read - await verify_test_data(browser_state, SESSION_ID) - - print("\n✨ Test completed successfully!") - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/tsconfig.json b/tests/interop/python-redis-typescript/tsconfig.json deleted file mode 100644 index 4b26f9d..0000000 --- a/tests/interop/python-redis-typescript/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "esModuleInterop": true, - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true - } -} \ No newline at end of file diff --git a/tests/interop/python-redis-typescript/verify_state.mjs b/tests/interop/python-redis-typescript/verify_state.mjs deleted file mode 100644 index e0aceed..0000000 --- a/tests/interop/python-redis-typescript/verify_state.mjs +++ /dev/null @@ -1,140 +0,0 @@ -/** - * TypeScript verification script for cross-language interop test. - * This script verifies browser state created by the Python implementation. - */ - -import path from 'path'; -import { fileURLToPath } from 'url'; -import fs from 'fs'; -import { BrowserState } from '../../../typescript/dist/index.js'; -import { chromium } from 'playwright'; - -// Get current file directory with ES modules -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Test constants - must match Python test -const SESSION_ID = 'cross_language_test'; -const USER_ID = 'interop_test_user'; - -// Debug mode - set to true to see browser UI -const DEBUG = false; - -// Redis configuration -const REDIS_CONFIG = { - host: 'localhost', - port: 6379, - password: undefined, - db: 0, - keyPrefix: 'browserstate', - ttl: 604800 // 7 days -}; - -// Path to the test HTML file - using absolute path to ensure same origin -const TEST_HTML_PATH = path.resolve(path.join(__dirname, '../../../typescript/examples/shared/test.html')); -const TEST_URL = `file://${TEST_HTML_PATH}`; - -// Function to fail the test with a clear error message -function failTest(message) { - console.error(`\n❌ TEST FAILED: ${message}`); - process.exit(1); -} - -async function verifyPythonState() { - console.log('🚀 Starting TypeScript Verification of Python State\n'); - - try { - // Initialize BrowserState with Redis storage - console.log('🔧 Creating BrowserState with Redis storage...'); - const browserState = new BrowserState({ - userId: USER_ID, - storageType: 'redis', - redisOptions: REDIS_CONFIG - }); - - // Mount the session - console.log(`\n📥 Mounting session: ${SESSION_ID}`); - const userDataDir = await browserState.mount(SESSION_ID); - console.log(`📂 Mounted at: ${userDataDir}`); - - // Launch browser with the mounted state - console.log('\n🌐 Launching browser with Playwright...'); - const browser = await chromium.launchPersistentContext(userDataDir, { - headless: !DEBUG - }); - - try { - // Create a new page - const page = await browser.newPage(); - - // Navigate to the test HTML page - using exact same URL as Python - console.log(`\n📄 Loading test page: ${TEST_URL}`); - await page.goto(TEST_URL); - console.log('✅ Test page loaded'); - - // Wait for the page to load completely - await page.waitForTimeout(1000); - - // Get the notes data - console.log('\n📝 Reading notes from localStorage...'); - const notesData = await page.evaluate(() => { - return localStorage.getItem('notes'); - }); - - if (notesData) { - const notes = JSON.parse(notesData); - console.log(`\nFound ${notes.length} notes:`); - notes.forEach((note) => { - console.log(` - ${note.text} (${note.timestamp})`); - }); - - // Verify the notes were created by Python - const pythonNotes = notes.filter((note) => - note.text.startsWith('Python created note') - ); - - if (pythonNotes.length === 0) { - failTest('No Python-created notes found in localStorage'); - } - - console.log(`\n✅ Found ${pythonNotes.length} Python-created notes`); - } else { - console.log('\n❌ No notes found in localStorage'); - - // Debug: check all localStorage items - const allItems = await page.evaluate(() => { - const items = {}; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - items[key] = localStorage.getItem(key); - } - return items; - }); - console.log('Available localStorage items:', allItems); - - // Try to examine the HTML structure - const html = await page.content(); - console.log(`Page HTML (first 200 chars): ${html.substring(0, 200)}...`); - - // Fail the test - failTest('No notes found in localStorage'); - } - - } finally { - await browser.close(); - } - - // Unmount the session - console.log('\n🔒 Unmounting session...'); - await browserState.unmount(); - console.log('✅ Session unmounted'); - - console.log('\n✨ Verification complete!'); - - } catch (error) { - failTest(`Error during verification: ${error instanceof Error ? error.message : String(error)}`); - } -} - -// Run the verification -verifyPythonState(); \ No newline at end of file diff --git a/tests/interop/python_tests/run_python_tests.py b/tests/interop/python_tests/run_python_tests.py new file mode 100644 index 0000000..7e49391 --- /dev/null +++ b/tests/interop/python_tests/run_python_tests.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +import argparse +import asyncio +from test_browser_state import create_state, verify_state + +def parse_args(): + parser = argparse.ArgumentParser(description="Python BrowserState Test Runner") + parser.add_argument("--mode", choices=["create", "verify"], required=True, help="Test mode: create or verify state") + parser.add_argument("--browser", choices=["chromium", "webkit", "firefox"], required=True, help="Browser to use") + parser.add_argument("--session", required=True, help="Session ID to use for test") + return parser.parse_args() + +async def main(): + args = parse_args() + if args.mode == "create": + await create_state(args.browser, args.session) + elif args.mode == "verify": + await verify_state(args.browser, args.session) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/interop/python_tests/test_browser_state.py b/tests/interop/python_tests/test_browser_state.py new file mode 100644 index 0000000..6c1609d --- /dev/null +++ b/tests/interop/python_tests/test_browser_state.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +import os +import sys +import json +import asyncio +from pathlib import Path +from playwright.async_api import async_playwright +from browserstate import BrowserState, BrowserStateOptions + +# Constants +USER_ID = "interop_test_user" +REDIS_CONFIG = { + "redis_url": "redis://localhost:6379/0", + "key_prefix": "browserstate" +} +# Resolve test HTML page path (assumes test_page/test.html relative to tests/interop) +TEST_PAGE_PATH = Path(__file__).resolve().parent.parent / "test_page" / "test.html" +TEST_URL = f"file://{TEST_PAGE_PATH}" + +def fail_test(message): + print(f"\n❌ TEST FAILED: {message}") + sys.exit(1) + +async def create_state(browser_name: str, session_id: str): + print(f"🚀 [Python] Creating state for session '{session_id}' on browser '{browser_name}'") + options = BrowserStateOptions( + user_id=USER_ID, + redis_options=REDIS_CONFIG + ) + browser_state = BrowserState(options) + + # Mount the session (this returns a dictionary with a "path" key) + mount_result = browser_state.mount_session(session_id) + user_data_dir = mount_result["path"] + print(f"📂 Mounted session at: {user_data_dir}") + + async with async_playwright() as p: + browser_launcher = getattr(p, browser_name) + context = await browser_launcher.launch_persistent_context( + user_data_dir=user_data_dir, + headless=True + ) + try: + page = await context.new_page() + print(f"📄 Loading test page: {TEST_URL}") + await page.goto(TEST_URL) + await page.wait_for_timeout(1000) + # Clear localStorage + await page.evaluate("localStorage.clear();") + # Add test notes by simulating input and clicking the button + test_notes = [f"Python {browser_name} note {i+1}" for i in range(3)] + for note in test_notes: + await page.fill("#noteInput", note) + await page.click("#addNoteButton") + await page.wait_for_timeout(500) + # Verify notes added + notes_count = await page.evaluate("() => JSON.parse(localStorage.getItem('notes') || '[]').length") + if notes_count != len(test_notes): + fail_test(f"Expected {len(test_notes)} notes, but found {notes_count}") + print(f"✅ Created {notes_count} notes on {browser_name}") + # Store browser metadata + await page.evaluate(f"""() => {{ + localStorage.setItem('browserMetadata', JSON.stringify({{ + browser: '{browser_name}', + createdBy: 'Python', + timestamp: new Date().toISOString() + }})); + }}""") + await page.wait_for_timeout(500) + finally: + await context.close() + browser_state.unmount_session() + print("✅ State creation complete.") + +async def verify_state(browser_name: str, session_id: str): + print(f"🔍 [Python] Verifying state for session '{session_id}' on browser '{browser_name}'") + options = BrowserStateOptions( + user_id=USER_ID, + redis_options=REDIS_CONFIG + ) + browser_state = BrowserState(options) + sessions = browser_state.list_sessions() + if session_id not in sessions: + fail_test(f"Session '{session_id}' not found in Redis") + mount_result = browser_state.mount_session(session_id) + user_data_dir = mount_result["path"] + print(f"📂 Mounted session at: {user_data_dir}") + + async with async_playwright() as p: + browser_launcher = getattr(p, browser_name) + context = await browser_launcher.launch_persistent_context( + user_data_dir=user_data_dir, + headless=True + ) + try: + page = await context.new_page() + print(f"📄 Loading test page: {TEST_URL}") + await page.goto(TEST_URL) + await page.wait_for_timeout(1000) + # Retrieve notes + notes_json = await page.evaluate("() => localStorage.getItem('notes')") + if not notes_json: + fail_test("No notes found in localStorage") + notes = json.loads(notes_json) + print(f"📝 Found {len(notes)} notes:") + for note in notes: + print(f" - {note['text']} at {note['timestamp']}") + # Verify browser metadata + metadata_json = await page.evaluate("() => localStorage.getItem('browserMetadata')") + if metadata_json: + metadata = json.loads(metadata_json) + if metadata.get("browser") != browser_name: + fail_test(f"Expected browser metadata '{browser_name}', but got '{metadata.get('browser')}'") + else: + fail_test("No browser metadata found") + print(f"✅ Verification successful for {browser_name}") + finally: + await context.close() + browser_state.unmount_session() diff --git a/tests/interop/run_all.sh b/tests/interop/run_all.sh index 08c60c6..2f8db68 100755 --- a/tests/interop/run_all.sh +++ b/tests/interop/run_all.sh @@ -1,6 +1,4 @@ #!/bin/bash - -# Exit on error set -e # Colors for output @@ -9,12 +7,12 @@ RED='\033[0;31m' BLUE='\033[0;34m' NC='\033[0m' # No Color -# Function to print section headers +# Print header function print_header() { echo -e "\n${BLUE}=== $1 ===${NC}\n" } -# Function to check if Redis is running +# Check if Redis is running check_redis() { print_header "Checking Redis Connection" if ! redis-cli ping > /dev/null 2>&1; then @@ -24,44 +22,60 @@ check_redis() { echo -e "${GREEN}✅ Redis is running${NC}" } -# Function to run a test suite -run_test_suite() { - local dir=$1 - local name=$2 - print_header "Running $name Tests" - - cd "$dir" - - # Run the test script - if [ -f "run_tests.sh" ]; then - if ! ./run_tests.sh; then - echo -e "${RED}❌ $name tests failed${NC}" - cd .. - exit 1 - fi - else - echo -e "${RED}❌ No test script found in $dir${NC}" - cd .. - exit 1 - fi - - cd .. - echo -e "${GREEN}✅ $name tests completed successfully${NC}" +# Run a Python test for a given browser, mode, and session ID +run_python_test() { + local browser=$1 + local mode=$2 + local session=$3 + print_header "Running Python Test: Browser=${browser}, Mode=${mode}, Session=${session}" + python3 python_tests/run_python_tests.py --browser "$browser" --mode "$mode" --session "$session" +} + +# Run a TypeScript test for a given browser, mode, and session ID +run_ts_test() { + local browser=$1 + local mode=$2 + local session=$3 + print_header "Running TypeScript Test: Browser=${browser}, Mode=${mode}, Session=${session}" + node typescript_tests/run_ts_tests.mjs --browser "$browser" --mode "$mode" --session "$session" +} + +# Run cross‑language interop tests +run_cross_language_tests() { + print_header "Cross‑Language Interop Test: Python creates state, TypeScript verifies state" + SESSION="py_create_ts_verify" + run_python_test "chromium" "create" "$SESSION" + run_ts_test "chromium" "verify" "$SESSION" + + print_header "Cross‑Language Interop Test: TypeScript creates state, Python verifies state" + SESSION="ts_create_py_verify" + run_ts_test "chromium" "create" "$SESSION" + run_python_test "chromium" "verify" "$SESSION" } # Main execution -echo -e "${BLUE}🚀 Starting BrowserState Interop Tests${NC}" +echo -e "${BLUE}🚀 Starting Full BrowserState Interop Tests${NC}" -# Check Redis check_redis -# Run all test suites -print_header "Cross-Language Interoperability Tests" -run_test_suite "python-redis-typescript" "Python -> Redis -> TypeScript" -run_test_suite "typescript-redis-python" "TypeScript -> Redis -> Python" +# Define browsers to test +BROWSERS=("chromium" "webkit" "firefox") + +# Run Python cross‑browser tests (create and verify) for each browser +for browser in "${BROWSERS[@]}"; do + SESSION="py_${browser}_test" + run_python_test "$browser" "create" "$SESSION" + run_python_test "$browser" "verify" "$SESSION" +done + +# Run TypeScript cross‑browser tests (create and verify) for each browser +for browser in "${BROWSERS[@]}"; do + SESSION="ts_${browser}_test" + run_ts_test "$browser" "create" "$SESSION" + run_ts_test "$browser" "verify" "$SESSION" +done -print_header "Cross-Browser Interoperability Tests" -run_test_suite "typescript-chrome-redis-safari" "Chrome -> Redis -> Safari" -run_test_suite "python-chrome-redis-safari" "Python-Chrome -> Redis -> Safari" +# Run cross‑language tests +run_cross_language_tests -echo -e "\n${GREEN}✨ All interop tests completed successfully!${NC}" \ No newline at end of file +echo -e "\n${GREEN}✨ All interop tests completed successfully!${NC}" diff --git a/tests/interop/setup.sh b/tests/interop/setup.sh index c007ba6..173529a 100755 --- a/tests/interop/setup.sh +++ b/tests/interop/setup.sh @@ -1,70 +1,41 @@ #!/bin/bash - -# Exit on error set -e -# Colors for output GREEN='\033[0;32m' RED='\033[0;31m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +NC='\033[0m' -# Function to print section headers print_header() { echo -e "\n${BLUE}=== $1 ===${NC}\n" } -# Function to setup a test directory -setup_test_dir() { - local dir=$1 - local name=$2 - print_header "Setting up $name test environment" - - # Create directory if it doesn't exist - mkdir -p "$dir" - cd "$dir" - - # Create and activate Python virtual environment - python3 -m venv venv - source venv/bin/activate - - # Upgrade pip - pip install --upgrade pip - - # Install Python dependencies - print_header "Installing Python dependencies" - pip install boto3 google-cloud-storage redis playwright - python -m playwright install chromium - - # Install Python package - pip install -e ../../../python - - # Initialize npm and install TypeScript package - npm init -y - npm install -e ../../../typescript - - # Install ts-node and other TypeScript dependencies - npm install --no-save playwright ts-node - - cd .. -} - -# Main setup print_header "Setting up BrowserState Interop Test Environment" -# Create test directories -mkdir -p python-redis-typescript -mkdir -p typescript-redis-python -mkdir -p typescript-chrome-redis-safari -mkdir -p python-chrome-redis-safari - -# Setup each test directory -setup_test_dir "python-redis-typescript" "Python -> Redis -> TypeScript" -setup_test_dir "typescript-redis-python" "TypeScript -> Redis -> Python" -setup_test_dir "typescript-chrome-redis-safari" "TypeScript-Chrome -> Redis -> Safari" -setup_test_dir "python-chrome-redis-safari" "Python-Chrome -> Redis -> Safari" +# --- Setup Python Environment --- +print_header "Setting up Python Environment" +if [ ! -d "python_tests/venv" ]; then + python3 -m venv python_tests/venv +fi +source python_tests/venv/bin/activate +pip install --upgrade pip +pip install playwright redis boto3 google-cloud-storage +# Install the local Python browserstate package (assumes it’s in ../../../../python) +pip install -e ../../../python +python -m playwright install chromium firefox webkit +deactivate + +# --- Setup TypeScript Environment --- +print_header "Setting up TypeScript Environment" +cd typescript_tests +if [ ! -f "package.json" ]; then + npm init -y +fi +npm install playwright ts-node ioredis --no-save +# Install the local TypeScript browserstate package (assumes it’s in ../../../../typescript) +npm install -e ../../../typescript +cd .. print_header "Setup Complete" echo -e "${GREEN}✅ All test environments are ready${NC}" -echo -e "\nYou can now run the tests using:" -echo -e " ./run_all.sh" \ No newline at end of file +echo -e "\nYou can now run the tests using:\n ./run_all.sh" diff --git a/tests/interop/test_page/test.html b/tests/interop/test_page/test.html new file mode 100644 index 0000000..e9dfc80 --- /dev/null +++ b/tests/interop/test_page/test.html @@ -0,0 +1,31 @@ + + + + + + BrowserState Test Page + + + +

BrowserState Test Page

+ + + + + + \ No newline at end of file diff --git a/tests/interop/tsconfig.json b/tests/interop/tsconfig.json new file mode 100644 index 0000000..566e844 --- /dev/null +++ b/tests/interop/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "outDir": "dist" + }, + "include": [ + "*.mjs", + "typescript_tests/*.mjs" + ] +} \ No newline at end of file diff --git a/tests/interop/typescript-chrome-redis-safari/README.md b/tests/interop/typescript-chrome-redis-safari/README.md deleted file mode 100644 index ee7b95b..0000000 --- a/tests/interop/typescript-chrome-redis-safari/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Chrome to Safari Browser Interoperability Test - -This directory contains interoperability tests between Chrome and Safari browsers using Redis as the storage backend. - -## Overview - -This test verifies that browser state (specifically localStorage data) can be: -1. Created in Chrome -2. Stored in Redis using the BrowserState library -3. Successfully restored in Safari - -## Prerequisites - -1. Redis server running locally (default port 6379) -2. Node.js with npm -3. Playwright - -## Test Structure - -The test consists of two main scripts: - -1. `create_state.mjs` - Creates browser state in Chrome and stores it in Redis -2. `verify_state.mjs` - Loads the Chrome-created state in Safari and verifies it - -## Running the Test - -To run the entire test suite: - -```bash -./run_tests.sh -``` - -To run individual steps: - -```bash -# Create state in Chrome -node create_state.mjs - -# Verify state in Safari -node verify_state.mjs -``` - -## Test Details - -### Chrome State Creation (`create_state.mjs`) - -1. Initializes a BrowserState instance with Redis storage -2. Mounts a new session for the browser state -3. Launches a Chrome browser with the mounted state -4. Adds test data to localStorage: - - Multiple note items - - Browser metadata (user agent, timestamp) -5. Unmounts the session, saving state to Redis - -### Safari Verification (`verify_state.mjs`) - -1. Initializes a BrowserState instance with the same Redis configuration -2. Lists available sessions to find the Chrome-created session -3. Mounts the Chrome-created session -4. Launches a Safari browser with the mounted state -5. Verifies that the localStorage data created in Chrome is accessible in Safari: - - Notes are present and contain the expected content - - Browser metadata shows it was created in Chrome -6. Adds Safari verification metadata to localStorage -7. Unmounts the session - -## Troubleshooting - -If the test fails, check: - -1. Redis is running: `redis-cli ping` -2. Playwright browsers are installed: `npx playwright install chromium webkit` -3. The TypeScript package is built: `npm --prefix ../../../typescript run build` \ No newline at end of file diff --git a/tests/interop/typescript-chrome-redis-safari/create_state.mjs b/tests/interop/typescript-chrome-redis-safari/create_state.mjs deleted file mode 100644 index cbbb3e8..0000000 --- a/tests/interop/typescript-chrome-redis-safari/create_state.mjs +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Script to create browser state in Chrome and store in Redis - * for cross-browser interoperability testing with Safari. - */ - -import path from 'path'; -import { fileURLToPath } from 'url'; -import { BrowserState } from '../../../typescript/dist/index.js'; -import { chromium } from 'playwright'; - -// Get current file directory with ES modules -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Test constants -const SESSION_ID = 'chrome_to_safari_test'; -const USER_ID = 'browser_interop_user'; - -// Debug mode - set to true to see browser UI -const DEBUG = false; - -// Redis configuration -const REDIS_CONFIG = { - host: 'localhost', - port: 6379, - password: undefined, - db: 0, - keyPrefix: 'browserstate', - ttl: 604800 // 7 days -}; - -// Path to the test HTML file -const TEST_HTML_PATH = path.resolve(path.join(__dirname, '../../../typescript/examples/shared/test.html')); -const TEST_URL = `file://${TEST_HTML_PATH}`; - -// Function to fail the test with a clear error message -function failTest(message) { - console.error(`\n❌ TEST FAILED: ${message}`); - process.exit(1); -} - -async function createTestData() { - console.log('🚀 Starting Chrome State Creation\n'); - - try { - // Initialize BrowserState with Redis storage - console.log('🔧 Creating BrowserState with Redis storage...'); - const browserState = new BrowserState({ - userId: USER_ID, - storageType: 'redis', - redisOptions: REDIS_CONFIG - }); - - // List existing sessions - console.log('\n📋 Listing existing sessions...'); - const sessions = await browserState.listSessions(); - console.log(`Found ${sessions.length} session(s): ${sessions.join(', ')}`); - - // Mount the session - console.log(`\n📥 Mounting session: ${SESSION_ID}`); - const userDataDir = await browserState.mount(SESSION_ID); - console.log(`📂 Mounted at: ${userDataDir}`); - - // Launch Chrome browser with the mounted state - console.log('\n🌐 Launching Chrome browser with Playwright...'); - const browser = await chromium.launchPersistentContext(userDataDir, { - headless: !DEBUG - }); - - try { - // Create a new page - const page = await browser.newPage(); - - // Navigate to the test HTML page - console.log(`\n📄 Loading test page: ${TEST_URL}`); - await page.goto(TEST_URL); - console.log('✅ Test page loaded'); - - // Clear existing localStorage to start fresh - await page.evaluate(() => { - localStorage.clear(); - }); - - // Wait for the page to load completely - await page.waitForTimeout(1000); - - // Add Chrome-specific test data to localStorage - console.log('\n📝 Adding Chrome-specific test data to localStorage...'); - - // Add notes to localStorage - const testNotes = [ - 'Chrome created note 1', - 'Chrome created note 2', - 'Chrome created note 3' - ]; - - for (const note of testNotes) { - await page.fill('#noteInput', note); - await page.click('button:text("Add Note")'); - await page.waitForTimeout(500); // Wait for animation - } - - // Add browser metadata to localStorage - await page.evaluate(() => { - localStorage.setItem('browserMetadata', JSON.stringify({ - browser: 'Chrome', - timestamp: new Date().toISOString(), - userAgent: navigator.userAgent - })); - }); - - // Verify notes were added - const notesCount = await page.evaluate(() => { - return JSON.parse(localStorage.getItem('notes') || '[]').length; - }); - - // Verify notes were added successfully - if (notesCount !== testNotes.length) { - failTest(`Expected ${testNotes.length} notes, but found ${notesCount}`); - } - - console.log(`✅ Added ${notesCount} notes`); - - // Get the notes data for verification - const notesData = await page.evaluate(() => { - return localStorage.getItem('notes'); - }); - - // Get browser metadata - const metadataJson = await page.evaluate(() => { - return localStorage.getItem('browserMetadata'); - }); - - // Verify data is not empty - if (!notesData || !metadataJson) { - failTest('Test data is empty'); - } - - console.log(`📝 Notes data: ${notesData}`); - console.log(`📝 Browser metadata: ${metadataJson}`); - - // Wait to ensure data is properly saved - await page.waitForTimeout(1000); - - } finally { - await browser.close(); - } - - // Unmount the session - console.log('\n🔒 Unmounting session...'); - await browserState.unmount(); - console.log('✅ Session unmounted'); - - console.log('\n✨ Chrome state creation complete!'); - console.log('Now run verify_state.mjs to verify the state in Safari'); - - } catch (error) { - failTest(`Error during Chrome state creation: ${error instanceof Error ? error.message : String(error)}`); - } -} - -// Run the state creation -createTestData(); \ No newline at end of file diff --git a/tests/interop/typescript-chrome-redis-safari/package-lock.json b/tests/interop/typescript-chrome-redis-safari/package-lock.json deleted file mode 100644 index 290b78a..0000000 --- a/tests/interop/typescript-chrome-redis-safari/package-lock.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "name": "chrome-redis-safari-interop", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "chrome-redis-safari-interop", - "version": "1.0.0", - "dependencies": { - "fs-extra": "^11.3.0", - "playwright": "^1.42.1" - } - }, - "node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/playwright": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", - "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", - "dependencies": { - "playwright-core": "1.51.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", - "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "engines": { - "node": ">= 10.0.0" - } - } - } -} diff --git a/tests/interop/typescript-chrome-redis-safari/package.json b/tests/interop/typescript-chrome-redis-safari/package.json deleted file mode 100644 index ce43944..0000000 --- a/tests/interop/typescript-chrome-redis-safari/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "chrome-redis-safari-test", - "version": "1.0.0", - "description": "BrowserState cross-browser test between Chrome and Safari", - "type": "module", - "scripts": { - "create": "node create_state.mjs", - "verify": "node verify_state.mjs", - "test": "npm run create && npm run verify" - }, - "dependencies": { - "fs-extra": "^11.2.0", - "playwright": "^1.35.1" - } -} diff --git a/tests/interop/typescript-chrome-redis-safari/run_tests.sh b/tests/interop/typescript-chrome-redis-safari/run_tests.sh deleted file mode 100755 index 46a0645..0000000 --- a/tests/interop/typescript-chrome-redis-safari/run_tests.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/bin/bash -# Shell script to run Chrome to Safari browser interop tests - -set -e # Exit immediately if a command exits with a non-zero status - -# Colors for console output -GREEN='\033[0;32m' -BLUE='\033[0;34m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Get directory of this script -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" -TYPESCRIPT_DIR="${SCRIPT_DIR}/../../../typescript" - -# Print test header -echo -e "${BLUE}========================================================${NC}" -echo -e "${BLUE} Cross-Browser Interop Test: Chrome to Safari via Redis ${NC}" -echo -e "${BLUE}========================================================${NC}" - -# Check if Redis is running -echo -e "\n${YELLOW}Checking if Redis is running...${NC}" -if command -v redis-cli >/dev/null 2>&1 ; then - if redis-cli ping > /dev/null 2>&1; then - echo -e "${GREEN}✓ Redis is running${NC}" - else - echo -e "${RED}✗ Redis is not running${NC}" - echo -e "${YELLOW}Please start Redis:${NC}" - echo -e " redis-server" - exit 1 - fi -else - echo -e "${RED}✗ redis-cli not found${NC}" - echo -e "${YELLOW}Please install Redis:${NC}" - echo -e " brew install redis" - exit 1 -fi - -# Check if Node.js modules are installed -echo -e "\n${YELLOW}Checking for Node.js dependencies...${NC}" -if [ ! -d "${SCRIPT_DIR}/node_modules" ]; then - echo -e "${YELLOW}Installing Node.js dependencies...${NC}" - npm --prefix "${SCRIPT_DIR}" install - echo -e "${GREEN}✓ Node.js dependencies installed${NC}" -else - echo -e "${GREEN}✓ Node.js dependencies found${NC}" -fi - -# Build the TypeScript package if it hasn't been built -echo -e "\n${YELLOW}Checking if TypeScript package is built...${NC}" -if [ ! -d "${TYPESCRIPT_DIR}/dist" ]; then - echo -e "${YELLOW}Building TypeScript package...${NC}" - npm --prefix "${TYPESCRIPT_DIR}" install - npm --prefix "${TYPESCRIPT_DIR}" run build - echo -e "${GREEN}✓ TypeScript package built${NC}" -else - echo -e "${GREEN}✓ TypeScript package is already built${NC}" -fi - -# Install Playwright browsers if needed -if ! npx playwright --version > /dev/null 2>&1; then - echo -e "\n${YELLOW}Installing Playwright browsers...${NC}" - npx playwright install chromium webkit - echo -e "${GREEN}✓ Playwright browsers installed${NC}" -fi - -# Run the Chrome state creation -echo -e "\n${YELLOW}Step 1: Creating browser state in Chrome...${NC}" -node "${SCRIPT_DIR}/create_state.mjs" -if [ $? -ne 0 ]; then - echo -e "${RED}✗ Chrome state creation failed${NC}" - exit 1 -fi -echo -e "${GREEN}✓ Chrome state creation completed${NC}" - -# Run the Safari state verification -echo -e "\n${YELLOW}Step 2: Verifying browser state in Safari...${NC}" -node "${SCRIPT_DIR}/verify_state.mjs" -if [ $? -ne 0 ]; then - echo -e "${RED}✗ Safari state verification failed${NC}" - exit 1 -fi -echo -e "${GREEN}✓ Safari state verification completed${NC}" - -# Test complete -echo -e "\n${GREEN}========================================================${NC}" -echo -e "${GREEN} 🎉 Chrome to Safari interop test completed successfully! ${NC}" -echo -e "${GREEN}========================================================${NC}" - -exit 0 \ No newline at end of file diff --git a/tests/interop/typescript-chrome-redis-safari/tsconfig.json b/tests/interop/typescript-chrome-redis-safari/tsconfig.json deleted file mode 100644 index 47555c4..0000000 --- a/tests/interop/typescript-chrome-redis-safari/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "node", - "esModuleInterop": true, - "strict": true, - "outDir": "dist" - }, - "include": ["*.mjs"] -} \ No newline at end of file diff --git a/tests/interop/typescript-chrome-redis-safari/verify_state.mjs b/tests/interop/typescript-chrome-redis-safari/verify_state.mjs deleted file mode 100644 index 067b551..0000000 --- a/tests/interop/typescript-chrome-redis-safari/verify_state.mjs +++ /dev/null @@ -1,260 +0,0 @@ -/** - * Script to verify Chrome-created browser state in Safari - * for cross-browser interoperability testing. - */ - -import path from 'path'; -import { fileURLToPath } from 'url'; -import { BrowserState } from '../../../typescript/dist/index.js'; -import { webkit } from 'playwright'; -import fs from 'fs-extra'; - -// Get current file directory with ES modules -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Test constants - must match create_state.mjs -const SESSION_ID = 'chrome_to_safari_test'; -const USER_ID = 'browser_interop_user'; - -// Debug mode - set to true to see browser UI -const DEBUG = false; - -// Redis configuration -const REDIS_CONFIG = { - host: 'localhost', - port: 6379, - password: undefined, - db: 0, - keyPrefix: 'browserstate', - ttl: 604800 // 7 days -}; - -// Path to the test HTML file - using absolute path to ensure same origin -const TEST_HTML_PATH = path.resolve(path.join(__dirname, '../../../typescript/examples/shared/test.html')); -const TEST_URL = `file://${TEST_HTML_PATH}`; - -// Function to fail the test with a clear error message -function failTest(message) { - console.error(`\n❌ TEST FAILED: ${message}`); - process.exit(1); -} - -/** - * Function to migrate localStorage data from Chrome to Safari format - * This is needed because Chrome and Safari store localStorage in different locations - * and formats in their browser profiles. - */ -async function migrateLocalStorageData(userDataDir) { - try { - // For the test file URL, figure out the localStorage domain key - const filePathUrl = new URL(TEST_URL); - const originKey = filePathUrl.origin || 'file://'; - - // Create a script that will help Safari find the test data - // Create a HTML file that Safari will see initially to copy the data - const initScriptPath = path.join(userDataDir, 'init-storage.html'); - const testNotesData = JSON.stringify([ - {"text":"Chrome created note 1","timestamp":"2025-04-01T19:26:34.473Z"}, - {"text":"Chrome created note 2","timestamp":"2025-04-01T19:26:35.022Z"}, - {"text":"Chrome created note 3","timestamp":"2025-04-01T19:26:35.574Z"} - ]); - - const metadataJson = JSON.stringify({ - "browser":"Chrome", - "timestamp":"2025-04-01T19:26:36.081Z", - "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/134.0.6998.35 Safari/537.36" - }); - - // Create an HTML file that will initialize localStorage - await fs.writeFile(initScriptPath, ` - - - - Initialize Storage - - -

Initializing cross-browser storage...

- - - - `); - - console.log(`✅ Created init-storage.html in user data directory for Safari`); - - return initScriptPath; - } catch (error) { - console.error(`❌ Error migrating localStorage data: ${error.message}`); - return null; - } -} - -async function verifyState() { - console.log('\n🔍 Verifying Chrome-created browser state in Safari\n'); - - try { - // Initialize BrowserState with Redis storage - console.log('🔧 Creating BrowserState with Redis storage...'); - const browserState = new BrowserState({ - userId: USER_ID, - storageType: 'redis', - redisOptions: REDIS_CONFIG - }); - - // List available sessions - console.log('\n📋 Listing available sessions...'); - const sessions = await browserState.listSessions(); - console.log(`Found ${sessions.length} session(s): ${sessions.join(', ')}`); - - if (!sessions.includes(SESSION_ID)) { - failTest(`Session '${SESSION_ID}' not found`); - } - - // Mount the session - console.log(`\n📥 Mounting session: ${SESSION_ID}`); - const userDataDir = await browserState.mount(SESSION_ID); - console.log(`📂 Mounted at: ${userDataDir}`); - - // Migrate localStorage data for cross-browser compatibility - const initScriptPath = await migrateLocalStorageData(userDataDir); - if (!initScriptPath) { - failTest("Failed to create initialization script for Safari"); - } - - // Create the file:// URL for the init script - const initScriptUrl = `file://${initScriptPath}`; - - // Launch Safari browser with the mounted state - console.log('\n🌐 Launching Safari browser with Playwright...'); - const browser = await webkit.launchPersistentContext(userDataDir, { - headless: !DEBUG - }); - - try { - // Create a new page - const page = await browser.newPage(); - - // First navigate to the init script to set up localStorage - console.log(`\n📄 Loading initialization page in Safari: ${initScriptUrl}`); - await page.goto(initScriptUrl); - await page.waitForTimeout(1000); - - // The init script will automatically redirect to the test page - console.log(`📄 Waiting for test page to load: ${TEST_URL}`); - await page.waitForURL(TEST_URL); - console.log('✅ Test page loaded in Safari'); - - // Wait for the page to load completely - await page.waitForTimeout(1000); - - // Retrieve notes from localStorage - const notesJson = await page.evaluate(() => { - return localStorage.getItem('notes'); - }); - - // Retrieve browser metadata - const metadataJson = await page.evaluate(() => { - return localStorage.getItem('browserMetadata'); - }); - - if (!notesJson) { - // Try to debug by looking at all localStorage items - const storageItems = await page.evaluate(() => { - const items = {}; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - items[key] = localStorage.getItem(key); - } - return items; - }); - console.log(`Available localStorage items: ${JSON.stringify(storageItems)}`); - failTest("No notes found in localStorage"); - } - - if (!metadataJson) { - failTest("No browser metadata found in localStorage"); - } - - // Parse JSON data - const notes = JSON.parse(notesJson); - const metadata = JSON.parse(metadataJson); - - // Verify notes - console.log(`\n📝 Found ${notes.length} notes in Safari:`); - notes.forEach(note => { - console.log(` - ${note.text} (${note.timestamp})`); - }); - - // Verify Chrome-created notes - const chromeNotes = notes.filter(note => note.text.startsWith('Chrome created')); - if (chromeNotes.length === 0) { - failTest('No Chrome-created notes found'); - } - - console.log(`✅ Found ${chromeNotes.length} Chrome-created notes in Safari`); - - // Verify browser metadata - console.log('\n📊 Browser metadata:'); - console.log(` - Original browser: ${metadata.browser}`); - console.log(` - Creation timestamp: ${metadata.timestamp}`); - console.log(` - Original user agent: ${metadata.userAgent}`); - - if (metadata.browser !== 'Chrome') { - failTest(`Expected metadata.browser to be 'Chrome', but found '${metadata.browser}'`); - } - - // Add Safari verification metadata - await page.evaluate(() => { - localStorage.setItem('safariVerification', JSON.stringify({ - verifiedIn: 'Safari', - timestamp: new Date().toISOString(), - userAgent: navigator.userAgent - })); - }); - - // Wait to ensure data is properly saved - await page.waitForTimeout(1000); - - // Verify the Safari verification metadata was saved - const safariVerificationJson = await page.evaluate(() => { - return localStorage.getItem('safariVerification'); - }); - - if (!safariVerificationJson) { - failTest('Failed to save Safari verification metadata'); - } - - const safariVerification = JSON.parse(safariVerificationJson); - console.log('\n✅ Added Safari verification metadata:'); - console.log(` - Verified in: ${safariVerification.verifiedIn}`); - console.log(` - Verification timestamp: ${safariVerification.timestamp}`); - - } finally { - await browser.close(); - } - - // Unmount the session - console.log('\n🔒 Unmounting session...'); - await browserState.unmount(); - console.log('✅ Session unmounted'); - - console.log('\n✨ Cross-browser verification complete!'); - console.log('✅ Successfully verified Chrome browser state in Safari'); - - } catch (error) { - failTest(`Error during verification: ${error instanceof Error ? error.message : String(error)}`); - } -} - -// Run the verification -verifyState(); \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/README.md b/tests/interop/typescript-redis-python/README.md deleted file mode 100644 index 9d4c6bb..0000000 --- a/tests/interop/typescript-redis-python/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# TypeScript -> Redis -> Python Interoperability Test - -This test verifies the cross-language interoperability between TypeScript and Python implementations of BrowserState using Redis as the storage backend. - -## Overview - -The test performs the following: - -1. Uses TypeScript to create browser state data (localStorage entries) using Playwright -2. Stores the state in Redis -3. Uses Python to load the state from Redis using Playwright -4. Verifies that the state created in TypeScript can be correctly read in Python - -## Requirements - -- Redis server running on localhost:6379 -- Python 3.6+ with pip -- Node.js with npm -- BrowserState Python package installed -- BrowserState TypeScript package installed - -## Files - -- `create_state.ts`: TypeScript script that creates state data and saves it to Redis -- `verify_state.py`: Python script that loads state from Redis and verifies it -- `run_tests.sh`: Shell script that installs dependencies and runs both tests in sequence - -## Running the Tests - -To run the tests, execute: - -```bash -./run_tests.sh -``` - -The script will: -1. Check if Redis is running -2. Install required dependencies (Playwright for both Python and TypeScript) -3. Run the TypeScript test to create state -4. Run the Python test to verify state - -## How it Works - -### TypeScript Side - -The TypeScript script: -1. Initializes a BrowserState instance with Redis storage -2. Mounts a session with a specific ID -3. Uses Playwright to load a test HTML page -4. Adds notes to localStorage in the browser -5. Unmounts the session, which saves the state to Redis - -### Python Side - -The Python script: -1. Initializes a BrowserState instance with the same Redis configuration -2. Mounts the same session ID -3. Uses Playwright to load the same test HTML page -4. Reads the localStorage data created by TypeScript -5. Verifies that the data created by TypeScript exists and is correct - -## Troubleshooting - -- If Redis is not running, start it with `brew services start redis` (macOS) or `redis-server` (Linux) -- If dependencies are not installed correctly, you can install them manually: - - For Python: `pip install playwright && python -m playwright install chromium` - - For TypeScript: `npm install playwright ts-node` \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/create_state.mjs b/tests/interop/typescript-redis-python/create_state.mjs deleted file mode 100644 index 4abc78f..0000000 --- a/tests/interop/typescript-redis-python/create_state.mjs +++ /dev/null @@ -1,145 +0,0 @@ -/** - * TypeScript script to create browser state data using localStorage - * for cross-language interoperability testing. - */ - -import path from 'path'; -import { fileURLToPath } from 'url'; -import { BrowserState } from '../../../typescript/dist/index.js'; -import { chromium } from 'playwright'; - -// Get current file directory with ES modules -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Test constants - must match Python test -const SESSION_ID = 'typescript_to_python_test'; -const USER_ID = 'interop_test_user'; - -// Debug mode - set to true to see browser UI -const DEBUG = false; - -// Redis configuration -const REDIS_CONFIG = { - host: 'localhost', - port: 6379, - password: undefined, - db: 0, - keyPrefix: 'browserstate', - ttl: 604800 // 7 days -}; - -// Path to the test HTML file - using absolute path to ensure same origin -const TEST_HTML_PATH = path.resolve(path.join(__dirname, '../../../typescript/examples/shared/test.html')); -const TEST_URL = `file://${TEST_HTML_PATH}`; - -// Function to fail the test with a clear error message -function failTest(message) { - console.error(`\n❌ TEST FAILED: ${message}`); - process.exit(1); -} - -async function createTestData() { - console.log('🚀 Starting TypeScript State Creation\n'); - - try { - // Initialize BrowserState with Redis storage - console.log('🔧 Creating BrowserState with Redis storage...'); - const browserState = new BrowserState({ - userId: USER_ID, - storageType: 'redis', - redisOptions: REDIS_CONFIG - }); - - // List existing sessions - console.log('\n📋 Listing existing sessions...'); - const sessions = await browserState.listSessions(); - console.log(`Found ${sessions.length} session(s): ${sessions.join(', ')}`); - - // Mount the session - console.log(`\n📥 Mounting session: ${SESSION_ID}`); - const userDataDir = await browserState.mount(SESSION_ID); - console.log(`📂 Mounted at: ${userDataDir}`); - - // Launch browser with the mounted state - console.log('\n🌐 Launching browser with Playwright...'); - const browser = await chromium.launchPersistentContext(userDataDir, { - headless: !DEBUG - }); - - try { - // Create a new page - const page = await browser.newPage(); - - // Navigate to the test HTML page - using exact same URL as Python verification - console.log(`\n📄 Loading test page: ${TEST_URL}`); - await page.goto(TEST_URL); - console.log('✅ Test page loaded'); - - // Clear existing localStorage to start fresh - await page.evaluate(() => { - localStorage.clear(); - }); - - // Wait for the page to load completely - await page.waitForTimeout(1000); - - // Add notes to localStorage - console.log('\n📝 Adding notes to localStorage...'); - const testNotes = [ - 'TypeScript created note 1', - 'TypeScript created note 2', - 'TypeScript created note 3' - ]; - - for (const note of testNotes) { - await page.fill('#noteInput', note); - await page.click('button:text("Add Note")'); - await page.waitForTimeout(500); // Wait for animation - } - - // Verify notes were added - const notesCount = await page.evaluate(() => { - return JSON.parse(localStorage.getItem('notes') || '[]').length; - }); - - // Verify notes were added successfully - if (notesCount !== testNotes.length) { - failTest(`Expected ${testNotes.length} notes, but found ${notesCount}`); - } - - console.log(`✅ Added ${notesCount} notes`); - - // Get the notes data for verification - const notesData = await page.evaluate(() => { - return localStorage.getItem('notes'); - }); - - // Verify notes data is not empty - if (!notesData) { - failTest('Notes data is empty'); - } - - console.log(`📝 Notes data: ${notesData}`); - - // Wait to ensure data is properly saved - await page.waitForTimeout(1000); - - } finally { - await browser.close(); - } - - // Unmount the session - console.log('\n🔒 Unmounting session...'); - await browserState.unmount(); - console.log('✅ Session unmounted'); - - console.log('\n✨ State creation complete!'); - - } catch (error) { - failTest(`Error during state creation: ${error instanceof Error ? error.message : String(error)}`); - } -} - -// Run the state creation -createTestData(); \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/package-lock.json b/tests/interop/typescript-redis-python/package-lock.json deleted file mode 100644 index 32b88c2..0000000 --- a/tests/interop/typescript-redis-python/package-lock.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "name": "typescript-redis-python", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "typescript-redis-python", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "ioredis": "^5.6.0" - }, - "devDependencies": {} - }, - "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", - "license": "MIT" - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/ioredis": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz", - "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "^1.1.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - } - } -} diff --git a/tests/interop/typescript-redis-python/package.json b/tests/interop/typescript-redis-python/package.json deleted file mode 100644 index 2c855be..0000000 --- a/tests/interop/typescript-redis-python/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "typescript-redis-python", - "version": "1.0.0", - "main": "index.js", - "type": "module", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "description": "This test verifies the cross-language interoperability between TypeScript and Python implementations of BrowserState using Redis as the storage backend.", - "dependencies": { - "ioredis": "^5.6.0" - }, - "devDependencies": {} -} diff --git a/tests/interop/typescript-redis-python/run_tests.sh b/tests/interop/typescript-redis-python/run_tests.sh deleted file mode 100755 index 46e87f6..0000000 --- a/tests/interop/typescript-redis-python/run_tests.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -# Exit on error -set -e - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Function to print section headers -print_header() { - echo -e "\n${BLUE}=== $1 ===${NC}\n" -} - -# Function to check if Redis is running -check_redis() { - print_header "Checking Redis Connection" - if ! redis-cli ping > /dev/null 2>&1; then - echo -e "${RED}❌ Redis is not running. Please start Redis first.${NC}" - exit 1 - fi - echo -e "${GREEN}✅ Redis is running${NC}" -} - -# Activate the virtual environment -print_header "Activating Python Environment" -if [ -f "venv/bin/activate" ]; then - source venv/bin/activate - echo -e "${GREEN}✅ Virtual environment activated${NC}" -else - echo -e "${RED}❌ Virtual environment not found. Please run the setup script first: cd .. && ./setup.sh${NC}" - exit 1 -fi - -# Function to check and install required dependencies -check_dependencies() { - print_header "Checking Required Dependencies" - - # Check Python dependencies - for pkg in boto3 redis google.cloud playwright browserstate; do - if ! python -c "import $pkg" &> /dev/null; then - echo -e "${RED}❌ Python package '$pkg' is not installed.${NC}" - echo -e "${RED}Please run the setup script first: cd .. && ./setup.sh${NC}" - exit 1 - fi - done - - # Check TypeScript dependencies using package.json - if [ ! -f "node_modules/playwright/package.json" ] || [ ! -f "node_modules/ts-node/package.json" ]; then - echo -e "${RED}❌ TypeScript dependencies are not installed.${NC}" - echo -e "${RED}Please run the setup script first: cd .. && ./setup.sh${NC}" - exit 1 - fi - - # Ensure ioredis is installed - if [ ! -f "node_modules/ioredis/package.json" ]; then - echo -e "${BLUE}Installing ioredis...${NC}" - npm install ioredis - fi - - echo -e "${GREEN}✅ All dependencies installed${NC}" -} - -# Main execution -echo -e "${BLUE}🚀 Starting TypeScript -> Redis -> Python Interop Test${NC}" - -# Check prerequisites -check_redis -check_dependencies - -# Run TypeScript state creation -print_header "Running TypeScript State Creation" -if ! node create_state.mjs; then - echo -e "${RED}❌ TypeScript state creation failed${NC}" - exit 1 -fi -echo -e "${GREEN}✅ TypeScript state creation completed successfully${NC}" - -# Run Python verification -print_header "Running Python Verification" -if ! python3 verify_state.py; then - echo -e "${RED}❌ Python verification failed${NC}" - exit 1 -fi -echo -e "${GREEN}✅ Python verification completed successfully${NC}" - -echo -e "\n${GREEN}✨ All tests completed successfully!${NC}" \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/tsconfig.json b/tests/interop/typescript-redis-python/tsconfig.json deleted file mode 100644 index 4b26f9d..0000000 --- a/tests/interop/typescript-redis-python/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "esModuleInterop": true, - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true - } -} \ No newline at end of file diff --git a/tests/interop/typescript-redis-python/verify_state.py b/tests/interop/typescript-redis-python/verify_state.py deleted file mode 100644 index a327ab7..0000000 --- a/tests/interop/typescript-redis-python/verify_state.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python3 -""" -Python verification script for cross-language interop test. -This script verifies browser state created by the TypeScript implementation. -""" - -import os -import json -import asyncio -import sys -from pathlib import Path -from playwright.async_api import async_playwright -from browserstate import BrowserState, BrowserStateOptions, RedisStorage - -# Redis configuration for Python -REDIS_URL = "redis://localhost:6379/0" -REDIS_KEY_PREFIX = "browserstate" - -# Test constants - must match TypeScript test -SESSION_ID = "typescript_to_python_test" -USER_ID = "interop_test_user" - -# Debug mode - set to True to see browser UI -DEBUG = False - -# Path to the test HTML file - using absolute path to ensure same origin -TEST_HTML_PATH = Path(__file__).parent.parent.parent.parent / "typescript" / "examples" / "shared" / "test.html" -TEST_URL = f"file://{TEST_HTML_PATH.absolute()}" - -def fail_test(message): - """Fail the test with a clear error message.""" - print(f"\n❌ TEST FAILED: {message}") - sys.exit(1) - -async def verify_typescript_state(): - """Verify the browser state created by TypeScript.""" - print("\n🔍 Verifying TypeScript-created browser state") - - # Create a Redis storage provider with the correct URL format - redis_options = { - "redis_url": REDIS_URL, - "key_prefix": REDIS_KEY_PREFIX - } - - # Initialize browser state with Redis storage - options = BrowserStateOptions( - user_id=USER_ID, - redis_options=redis_options - ) - browser_state = BrowserState(options) - - # List available sessions - sessions = browser_state.list_sessions() - print(f"📋 Available sessions: {sessions}") - - if SESSION_ID not in sessions: - fail_test(f"Session '{SESSION_ID}' not found") - - # Mount the session - state = browser_state.mount_session(SESSION_ID) - print(f"📂 Mounted session at: {state['path']}") - - async with async_playwright() as p: - # Launch browser with the mounted state - browser = await p.chromium.launch_persistent_context( - user_data_dir=state["path"], - headless=not DEBUG - ) - - try: - # Create a new page - page = await browser.new_page() - - # Navigate to the test HTML page - using the EXACT same URL - print(f"📄 Loading test page: {TEST_URL}") - await page.goto(TEST_URL) - - # Wait for the page to load completely - await page.wait_for_timeout(1000) - - # Get the notes data - notes_data = await page.evaluate("""() => { - return localStorage.getItem('notes'); - }""") - - if notes_data: - notes = json.loads(notes_data) - print(f"📝 Found {len(notes)} notes:") - for note in notes: - print(f" - {note['text']} ({note['timestamp']})") - - # Verify the notes were created by TypeScript - typescript_notes = [note for note in notes if note['text'].startswith('TypeScript created note')] - if not typescript_notes: - fail_test("No TypeScript-created notes found") - - print(f"✅ Found {len(typescript_notes)} TypeScript-created notes") - else: - # Try to debug by looking at all localStorage items - storage_items = await page.evaluate("""() => { - const items = {}; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - items[key] = localStorage.getItem(key); - } - return items; - }""") - print(f"Available localStorage items: {storage_items}") - fail_test("No notes found in localStorage") - - finally: - await browser.close() - - # Unmount the session - browser_state.unmount_session() - print("✅ Verification complete") - -if __name__ == "__main__": - try: - asyncio.run(verify_typescript_state()) - except Exception as e: - fail_test(f"Unexpected error: {e}") \ No newline at end of file diff --git a/tests/interop/typescript_tests/run_ts_tests.mjs b/tests/interop/typescript_tests/run_ts_tests.mjs new file mode 100644 index 0000000..015de50 --- /dev/null +++ b/tests/interop/typescript_tests/run_ts_tests.mjs @@ -0,0 +1,29 @@ +#!/usr/bin/env node +import { createState, verifyState } from './test_browser_state.mjs'; +import minimist from 'minimist'; + +const args = minimist(process.argv.slice(2)); +const mode = args.mode; +const browser = args.browser; +const session = args.session; + +if (!mode || !browser || !session) { + console.error("Usage: node run_ts_tests.mjs --mode --browser --session "); + process.exit(1); +} + +(async () => { + try { + if (mode === 'create') { + await createState(browser, session); + } else if (mode === 'verify') { + await verifyState(browser, session); + } else { + console.error("Invalid mode. Use 'create' or 'verify'."); + process.exit(1); + } + } catch (error) { + console.error("Test failed:", error); + process.exit(1); + } +})(); diff --git a/tests/interop/typescript_tests/test_browser_state.mjs b/tests/interop/typescript_tests/test_browser_state.mjs new file mode 100644 index 0000000..7b21312 --- /dev/null +++ b/tests/interop/typescript_tests/test_browser_state.mjs @@ -0,0 +1,128 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; +import { chromium, webkit, firefox } from 'playwright'; +import { BrowserState } from 'browserstate'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const USER_ID = "interop_test_user"; +const REDIS_CONFIG = { + host: 'localhost', + port: 6379, + password: undefined, + db: 0, + keyPrefix: 'browserstate', + ttl: 604800 // 7 days +}; + +// Resolve test HTML page path (assumes ../test_page/test.html relative to tests/interop) +const TEST_PAGE_PATH = path.resolve(__dirname, "../test_page/test.html"); +const TEST_URL = `file://${TEST_PAGE_PATH}`; + +function failTest(message) { + console.error(`\n❌ TEST FAILED: ${message}`); + process.exit(1); +} + +function getBrowserLauncher(browserName) { + if (browserName === 'chromium') return chromium; + if (browserName === 'webkit') return webkit; + if (browserName === 'firefox') return firefox; + throw new Error(`Unsupported browser: ${browserName}`); +} + +export async function createState(browserName, sessionId) { + console.log(`🚀 [TypeScript] Creating state for session '${sessionId}' on browser '${browserName}'`); + const browserState = new BrowserState({ + userId: USER_ID, + storageType: 'redis', + redisOptions: REDIS_CONFIG + }); + const userDataDir = await browserState.mount(sessionId); + console.log(`📂 Mounted session at: ${userDataDir}`); + const browserLauncher = getBrowserLauncher(browserName); + const context = await browserLauncher.launchPersistentContext(userDataDir, { headless: true }); + try { + const page = await context.newPage(); + console.log(`📄 Loading test page: ${TEST_URL}`); + await page.goto(TEST_URL); + await page.waitForTimeout(1000); + // Clear localStorage + await page.evaluate(() => localStorage.clear()); + // Add test notes + const testNotes = [ + `TypeScript ${browserName} note 1`, + `TypeScript ${browserName} note 2`, + `TypeScript ${browserName} note 3` + ]; + for (const note of testNotes) { + await page.fill('#noteInput', note); + await page.click('#addNoteButton'); + await page.waitForTimeout(500); + } + const notesCount = await page.evaluate(() => JSON.parse(localStorage.getItem('notes') || '[]').length); + if (notesCount !== testNotes.length) { + failTest(`Expected ${testNotes.length} notes, but found ${notesCount}`); + } + console.log(`✅ Created ${notesCount} notes on ${browserName}`); + // Store browser metadata + await page.evaluate((browserName) => { + localStorage.setItem('browserMetadata', JSON.stringify({ + browser: browserName, + createdBy: 'TypeScript', + timestamp: new Date().toISOString() + })); + }, browserName); + await page.waitForTimeout(500); + } finally { + await context.close(); + } + await browserState.unmount(); + console.log("✅ State creation complete."); +} + +export async function verifyState(browserName, sessionId) { + console.log(`🔍 [TypeScript] Verifying state for session '${sessionId}' on browser '${browserName}'`); + const browserState = new BrowserState({ + userId: USER_ID, + storageType: 'redis', + redisOptions: REDIS_CONFIG + }); + const sessions = await browserState.listSessions(); + if (!sessions.includes(sessionId)) { + failTest(`Session '${sessionId}' not found in Redis`); + } + const userDataDir = await browserState.mount(sessionId); + console.log(`📂 Mounted session at: ${userDataDir}`); + const browserLauncher = getBrowserLauncher(browserName); + const context = await browserLauncher.launchPersistentContext(userDataDir, { headless: true }); + try { + const page = await context.newPage(); + console.log(`📄 Loading test page: ${TEST_URL}`); + await page.goto(TEST_URL); + await page.waitForTimeout(1000); + const notesJson = await page.evaluate(() => localStorage.getItem('notes')); + if (!notesJson) { + failTest("No notes found in localStorage"); + } + const notes = JSON.parse(notesJson); + console.log(`📝 Found ${notes.length} notes:`); + for (const note of notes) { + console.log(` - ${note.text} at ${note.timestamp}`); + } + const metadataJson = await page.evaluate(() => localStorage.getItem('browserMetadata')); + if (metadataJson) { + const metadata = JSON.parse(metadataJson); + if (metadata.browser !== browserName) { + failTest(`Expected browser metadata '${browserName}', but got '${metadata.browser}'`); + } + } else { + failTest("No browser metadata found"); + } + console.log(`✅ Verification successful for ${browserName}`); + } finally { + await context.close(); + } + await browserState.unmount(); +} From 654328757f732583b0f2f895eef96c5fed3602bc Mon Sep 17 00:00:00 2001 From: Sagar Date: Tue, 1 Apr 2025 21:13:54 -0400 Subject: [PATCH 19/20] Finished adding all tests --- tests/interop/run_all.sh | 2 + tests/interop/setup.sh | 8 ++- .../typescript_tests/package-lock.json | 66 +++++++++++++++++++ tests/interop/typescript_tests/package.json | 16 +++++ 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 tests/interop/typescript_tests/package-lock.json create mode 100644 tests/interop/typescript_tests/package.json diff --git a/tests/interop/run_all.sh b/tests/interop/run_all.sh index 2f8db68..fe0b36a 100755 --- a/tests/interop/run_all.sh +++ b/tests/interop/run_all.sh @@ -1,4 +1,6 @@ #!/bin/bash +source ./python_tests/venv/bin/activate + set -e # Colors for output diff --git a/tests/interop/setup.sh b/tests/interop/setup.sh index 173529a..65d067e 100755 --- a/tests/interop/setup.sh +++ b/tests/interop/setup.sh @@ -20,8 +20,8 @@ fi source python_tests/venv/bin/activate pip install --upgrade pip pip install playwright redis boto3 google-cloud-storage -# Install the local Python browserstate package (assumes it’s in ../../../../python) -pip install -e ../../../python +# Install the local Python browserstate package (assumes it’s in ../../python) +pip install -e ../../python python -m playwright install chromium firefox webkit deactivate @@ -32,8 +32,10 @@ if [ ! -f "package.json" ]; then npm init -y fi npm install playwright ts-node ioredis --no-save -# Install the local TypeScript browserstate package (assumes it’s in ../../../../typescript) +# Install the local TypeScript browserstate package (assumes it’s in ../../typescript) npm install -e ../../../typescript +npm install minimist +npm install playwright cd .. print_header "Setup Complete" diff --git a/tests/interop/typescript_tests/package-lock.json b/tests/interop/typescript_tests/package-lock.json new file mode 100644 index 0000000..fbb62b2 --- /dev/null +++ b/tests/interop/typescript_tests/package-lock.json @@ -0,0 +1,66 @@ +{ + "name": "typescript_tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "typescript_tests", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "minimist": "^1.2.8", + "playwright": "^1.51.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/playwright": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", + "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "dependencies": { + "playwright-core": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", + "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/tests/interop/typescript_tests/package.json b/tests/interop/typescript_tests/package.json new file mode 100644 index 0000000..f05202a --- /dev/null +++ b/tests/interop/typescript_tests/package.json @@ -0,0 +1,16 @@ +{ + "name": "typescript_tests", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "minimist": "^1.2.8", + "playwright": "^1.51.1" + } +} From 127bd833b65daadccb18375c0bbe92daf9176854 Mon Sep 17 00:00:00 2001 From: Sagar Date: Wed, 2 Apr 2025 15:47:02 -0400 Subject: [PATCH 20/20] Finished matrix interop testing, runs through combination of cross-language and cross-browser interop tests --- .../python_tests/test_browser_state.py | 140 ++++++++++++++--- tests/interop/run_all.sh | 52 ++++++- tests/interop/setup.sh | 2 +- .../typescript_tests/test_browser_state.mjs | 141 +++++++++++++++--- 4 files changed, 286 insertions(+), 49 deletions(-) diff --git a/tests/interop/python_tests/test_browser_state.py b/tests/interop/python_tests/test_browser_state.py index 6c1609d..c211b7c 100644 --- a/tests/interop/python_tests/test_browser_state.py +++ b/tests/interop/python_tests/test_browser_state.py @@ -3,6 +3,7 @@ import sys import json import asyncio +import time from pathlib import Path from playwright.async_api import async_playwright from browserstate import BrowserState, BrowserStateOptions @@ -13,10 +14,14 @@ "redis_url": "redis://localhost:6379/0", "key_prefix": "browserstate" } + # Resolve test HTML page path (assumes test_page/test.html relative to tests/interop) TEST_PAGE_PATH = Path(__file__).resolve().parent.parent / "test_page" / "test.html" TEST_URL = f"file://{TEST_PAGE_PATH}" +# Name of our JSON file that holds metadata for cross-browser migration +METADATA_FILENAME = "browserstate_interop_metadata.json" + def fail_test(message): print(f"\n❌ TEST FAILED: {message}") sys.exit(1) @@ -45,30 +50,56 @@ async def create_state(browser_name: str, session_id: str): print(f"📄 Loading test page: {TEST_URL}") await page.goto(TEST_URL) await page.wait_for_timeout(1000) - # Clear localStorage + + # Clear localStorage in the test page await page.evaluate("localStorage.clear();") + # Add test notes by simulating input and clicking the button test_notes = [f"Python {browser_name} note {i+1}" for i in range(3)] for note in test_notes: await page.fill("#noteInput", note) await page.click("#addNoteButton") await page.wait_for_timeout(500) + # Verify notes added - notes_count = await page.evaluate("() => JSON.parse(localStorage.getItem('notes') || '[]').length") + notes_count = await page.evaluate( + "JSON.parse(localStorage.getItem('notes') || '[]').length" + ) if notes_count != len(test_notes): fail_test(f"Expected {len(test_notes)} notes, but found {notes_count}") print(f"✅ Created {notes_count} notes on {browser_name}") - # Store browser metadata - await page.evaluate(f"""() => {{ - localStorage.setItem('browserMetadata', JSON.stringify({{ - browser: '{browser_name}', - createdBy: 'Python', - timestamp: new Date().toISOString() - }})); - }}""") + + # ---- 1) Grab the notes from localStorage + notes_in_localstorage = await page.evaluate( + "JSON.parse(localStorage.getItem('notes') || '[]')" + ) + + # ---- 2) Build “metadata” object that we’ll also store in a shared JSON file + metadata = { + "browser": browser_name, + "createdBy": "Python", + "timestamp": time.time(), + "notes": notes_in_localstorage + } + + # For completeness, also store that metadata in localStorage + await page.evaluate( + """(meta) => { + localStorage.setItem('browserMetadata', JSON.stringify(meta)); + }""", + metadata + ) await page.wait_for_timeout(500) + + # ---- 3) Write that same metadata to a JSON file in the user_data_dir + metadata_path = os.path.join(user_data_dir, METADATA_FILENAME) + with open(metadata_path, "w", encoding="utf-8") as f: + json.dump(metadata, f, ensure_ascii=False, indent=2) + finally: await context.close() + + # Unmount => upload the updated user_data_dir to Redis browser_state.unmount_session() print("✅ State creation complete.") @@ -79,9 +110,13 @@ async def verify_state(browser_name: str, session_id: str): redis_options=REDIS_CONFIG ) browser_state = BrowserState(options) + + # First confirm the session is in Redis sessions = browser_state.list_sessions() if session_id not in sessions: fail_test(f"Session '{session_id}' not found in Redis") + + # Mount => download session to a local user_data_dir mount_result = browser_state.mount_session(session_id) user_data_dir = mount_result["path"] print(f"📂 Mounted session at: {user_data_dir}") @@ -97,23 +132,84 @@ async def verify_state(browser_name: str, session_id: str): print(f"📄 Loading test page: {TEST_URL}") await page.goto(TEST_URL) await page.wait_for_timeout(1000) - # Retrieve notes - notes_json = await page.evaluate("() => localStorage.getItem('notes')") - if not notes_json: - fail_test("No notes found in localStorage") + + # Attempt to read notes from localStorage + notes_json = await page.evaluate("localStorage.getItem('notes')") + + # If no notes, attempt to do a *real* cross-browser migration + if not notes_json or notes_json.strip() in ("", "null"): + print("No notes found in localStorage. Attempting cross-browser migration from JSON file.") + + metadata_path = os.path.join(user_data_dir, METADATA_FILENAME) + if os.path.exists(metadata_path): + # Read metadata from that file, which might have been created by a different browser + with open(metadata_path, "r", encoding="utf-8") as f: + metadata = json.load(f) + creator = metadata.get("browser") + original_notes = metadata.get("notes", []) + + if creator and creator != browser_name and original_notes: + print(f"Transforming state from {creator} to {browser_name}") + + # Example “real” migration: keep the same note text, but indicate a migration timestamp + migrated_notes = [ + {"text": note["text"], "timestamp": "migrated"} + for note in original_notes + ] + + # Inject the migrated notes into localStorage + migrated_notes_json = json.dumps(migrated_notes) + await page.evaluate( + "(notes) => { localStorage.setItem('notes', notes); }", + migrated_notes_json + ) + + # Also update the metadata to reflect new browser + metadata["browser"] = browser_name + metadata["notes"] = migrated_notes + + # Put updated metadata in localStorage + await page.evaluate( + """(meta) => { + localStorage.setItem('browserMetadata', JSON.stringify(meta)); + }""", + metadata + ) + + # Overwrite the metadata JSON file with updated info + with open(metadata_path, "w", encoding="utf-8") as f: + json.dump(metadata, f, ensure_ascii=False, indent=2) + + # Double-check that notes are now in localStorage + notes_json = await page.evaluate("localStorage.getItem('notes')") + if not notes_json or notes_json.strip() in ("", "null"): + fail_test("Migration step failed to populate notes in LocalStorage.") + else: + fail_test("No notes found in localStorage and no valid cross-browser metadata to migrate.") + else: + fail_test("No notes found in localStorage and no JSON metadata file available.") + + # If we get here, we should have notes in localStorage notes = json.loads(notes_json) print(f"📝 Found {len(notes)} notes:") for note in notes: print(f" - {note['text']} at {note['timestamp']}") - # Verify browser metadata - metadata_json = await page.evaluate("() => localStorage.getItem('browserMetadata')") - if metadata_json: - metadata = json.loads(metadata_json) - if metadata.get("browser") != browser_name: - fail_test(f"Expected browser metadata '{browser_name}', but got '{metadata.get('browser')}'") - else: - fail_test("No browser metadata found") + + # Verify the final browser metadata from localStorage + metadata_json = await page.evaluate("localStorage.getItem('browserMetadata')") + if not metadata_json: + fail_test("No browserMetadata found in localStorage at all.") + + metadata = json.loads(metadata_json) + actual_browser = metadata.get("browser") + if actual_browser != browser_name: + fail_test( + f"Metadata browser mismatch: expected '{browser_name}', got '{actual_browser}'" + ) + print(f"✅ Verification successful for {browser_name}") finally: await context.close() + + # Upload updated user_data_dir back to Redis browser_state.unmount_session() diff --git a/tests/interop/run_all.sh b/tests/interop/run_all.sh index fe0b36a..207ffbb 100755 --- a/tests/interop/run_all.sh +++ b/tests/interop/run_all.sh @@ -42,7 +42,7 @@ run_ts_test() { node typescript_tests/run_ts_tests.mjs --browser "$browser" --mode "$mode" --session "$session" } -# Run cross‑language interop tests +# Run cross‑language interop tests (existing) run_cross_language_tests() { print_header "Cross‑Language Interop Test: Python creates state, TypeScript verifies state" SESSION="py_create_ts_verify" @@ -63,21 +63,65 @@ check_redis # Define browsers to test BROWSERS=("chromium" "webkit" "firefox") -# Run Python cross‑browser tests (create and verify) for each browser +# Run Python cross‑browser tests (create and verify) for each browser (same browser tests) for browser in "${BROWSERS[@]}"; do SESSION="py_${browser}_test" run_python_test "$browser" "create" "$SESSION" run_python_test "$browser" "verify" "$SESSION" done -# Run TypeScript cross‑browser tests (create and verify) for each browser +# Run TypeScript cross‑browser tests (create and verify) for each browser (same browser tests) for browser in "${BROWSERS[@]}"; do SESSION="ts_${browser}_test" run_ts_test "$browser" "create" "$SESSION" run_ts_test "$browser" "verify" "$SESSION" done -# Run cross‑language tests +# Run Python cross‑browser tests: create with one browser, verify with a different browser +for creator in "${BROWSERS[@]}"; do + for verifier in "${BROWSERS[@]}"; do + if [ "$creator" != "$verifier" ]; then + SESSION="py_${creator}_to_py_${verifier}" + run_python_test "$creator" "create" "$SESSION" + run_python_test "$verifier" "verify" "$SESSION" + fi + done +done + +# Run TypeScript cross‑browser tests: create with one browser, verify with a different browser +for creator in "${BROWSERS[@]}"; do + for verifier in "${BROWSERS[@]}"; do + if [ "$creator" != "$verifier" ]; then + SESSION="ts_${creator}_to_ts_${verifier}" + run_ts_test "$creator" "create" "$SESSION" + run_ts_test "$verifier" "verify" "$SESSION" + fi + done +done + +# Run cross‑language tests: Python creates state with one browser, TypeScript verifies with a different browser +for creator in "${BROWSERS[@]}"; do + for verifier in "${BROWSERS[@]}"; do + if [ "$creator" != "$verifier" ]; then + SESSION="py_${creator}_to_ts_${verifier}" + run_python_test "$creator" "create" "$SESSION" + run_ts_test "$verifier" "verify" "$SESSION" + fi + done +done + +# Run cross‑language tests: TypeScript creates state with one browser, Python verifies with a different browser +for creator in "${BROWSERS[@]}"; do + for verifier in "${BROWSERS[@]}"; do + if [ "$creator" != "$verifier" ]; then + SESSION="ts_${creator}_to_py_${verifier}" + run_ts_test "$creator" "create" "$SESSION" + run_python_test "$verifier" "verify" "$SESSION" + fi + done +done + +# Run cross‑language tests (existing) run_cross_language_tests echo -e "\n${GREEN}✨ All interop tests completed successfully!${NC}" diff --git a/tests/interop/setup.sh b/tests/interop/setup.sh index 65d067e..cb3fc84 100755 --- a/tests/interop/setup.sh +++ b/tests/interop/setup.sh @@ -33,7 +33,7 @@ if [ ! -f "package.json" ]; then fi npm install playwright ts-node ioredis --no-save # Install the local TypeScript browserstate package (assumes it’s in ../../typescript) -npm install -e ../../../typescript +npm install -e ../../typescript npm install minimist npm install playwright cd .. diff --git a/tests/interop/typescript_tests/test_browser_state.mjs b/tests/interop/typescript_tests/test_browser_state.mjs index 7b21312..6f97c5c 100644 --- a/tests/interop/typescript_tests/test_browser_state.mjs +++ b/tests/interop/typescript_tests/test_browser_state.mjs @@ -1,11 +1,14 @@ +import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { chromium, webkit, firefox } from 'playwright'; -import { BrowserState } from 'browserstate'; +import { BrowserState } from '../../../typescript/dist/index.js'; +// For ESM __dirname const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +// Constants const USER_ID = "interop_test_user"; const REDIS_CONFIG = { host: 'localhost', @@ -16,6 +19,9 @@ const REDIS_CONFIG = { ttl: 604800 // 7 days }; +// Metadata file name to store cross-browser data: +const METADATA_FILENAME = "browserstate_interop_metadata.json"; + // Resolve test HTML page path (assumes ../test_page/test.html relative to tests/interop) const TEST_PAGE_PATH = path.resolve(__dirname, "../test_page/test.html"); const TEST_URL = `file://${TEST_PAGE_PATH}`; @@ -32,6 +38,7 @@ function getBrowserLauncher(browserName) { throw new Error(`Unsupported browser: ${browserName}`); } +// 1) CREATE STATE export async function createState(browserName, sessionId) { console.log(`🚀 [TypeScript] Creating state for session '${sessionId}' on browser '${browserName}'`); const browserState = new BrowserState({ @@ -39,8 +46,11 @@ export async function createState(browserName, sessionId) { storageType: 'redis', redisOptions: REDIS_CONFIG }); + + // Mount session => download from Redis (or create new) => returns userDataDir const userDataDir = await browserState.mount(sessionId); console.log(`📂 Mounted session at: ${userDataDir}`); + const browserLauncher = getBrowserLauncher(browserName); const context = await browserLauncher.launchPersistentContext(userDataDir, { headless: true }); try { @@ -48,9 +58,11 @@ export async function createState(browserName, sessionId) { console.log(`📄 Loading test page: ${TEST_URL}`); await page.goto(TEST_URL); await page.waitForTimeout(1000); - // Clear localStorage + + // Clear localStorage in the test page await page.evaluate(() => localStorage.clear()); - // Add test notes + + // Add 3 test notes const testNotes = [ `TypeScript ${browserName} note 1`, `TypeScript ${browserName} note 2`, @@ -61,27 +73,51 @@ export async function createState(browserName, sessionId) { await page.click('#addNoteButton'); await page.waitForTimeout(500); } - const notesCount = await page.evaluate(() => JSON.parse(localStorage.getItem('notes') || '[]').length); + + // Verify that the notes got stored in localStorage + const notesCount = await page.evaluate(() => { + const notes = JSON.parse(localStorage.getItem('notes') || '[]'); + return notes.length; + }); if (notesCount !== testNotes.length) { failTest(`Expected ${testNotes.length} notes, but found ${notesCount}`); } console.log(`✅ Created ${notesCount} notes on ${browserName}`); - // Store browser metadata - await page.evaluate((browserName) => { - localStorage.setItem('browserMetadata', JSON.stringify({ - browser: browserName, - createdBy: 'TypeScript', - timestamp: new Date().toISOString() - })); - }, browserName); + + // Grab those notes from localStorage + const notesInLocalStorage = await page.evaluate(() => + JSON.parse(localStorage.getItem('notes') || '[]') + ); + + // Build metadata object + const metadata = { + browser: browserName, + createdBy: 'TypeScript', + timestamp: Date.now(), + notes: notesInLocalStorage + }; + + // Store metadata in localStorage for same-browser tests + await page.evaluate((meta) => { + localStorage.setItem('browserMetadata', JSON.stringify(meta)); + }, metadata); + + // Also store the same metadata in a JSON file in userDataDir + const metadataPath = path.join(userDataDir, METADATA_FILENAME); + fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8'); + await page.waitForTimeout(500); } finally { await context.close(); } + + // Unmount => upload changes back to Redis await browserState.unmount(); console.log("✅ State creation complete."); + process.exit(0); } +// 2) VERIFY STATE export async function verifyState(browserName, sessionId) { console.log(`🔍 [TypeScript] Verifying state for session '${sessionId}' on browser '${browserName}'`); const browserState = new BrowserState({ @@ -89,12 +125,17 @@ export async function verifyState(browserName, sessionId) { storageType: 'redis', redisOptions: REDIS_CONFIG }); + + // Ensure session is in Redis const sessions = await browserState.listSessions(); if (!sessions.includes(sessionId)) { failTest(`Session '${sessionId}' not found in Redis`); } + + // Mount session => download userDataDir const userDataDir = await browserState.mount(sessionId); console.log(`📂 Mounted session at: ${userDataDir}`); + const browserLauncher = getBrowserLauncher(browserName); const context = await browserLauncher.launchPersistentContext(userDataDir, { headless: true }); try { @@ -102,27 +143,83 @@ export async function verifyState(browserName, sessionId) { console.log(`📄 Loading test page: ${TEST_URL}`); await page.goto(TEST_URL); await page.waitForTimeout(1000); - const notesJson = await page.evaluate(() => localStorage.getItem('notes')); - if (!notesJson) { - failTest("No notes found in localStorage"); + + // Attempt to read notes from localStorage + let notesJson = await page.evaluate(() => localStorage.getItem('notes')); + + if (!notesJson || notesJson.trim() === "" || notesJson === "null") { + console.log("No notes found in LocalStorage. Attempting cross-browser migration from JSON file."); + + // 1) Read the metadata file from userDataDir + const metadataPath = path.join(userDataDir, METADATA_FILENAME); + if (fs.existsSync(metadataPath)) { + const fileContents = fs.readFileSync(metadataPath, 'utf-8'); + const metadata = JSON.parse(fileContents); + + const creator = metadata.browser; + const originalNotes = metadata.notes || []; + if (creator && creator !== browserName && originalNotes.length > 0) { + console.log(`Transforming state from ${creator} to ${browserName}`); + + // Migrate notes => mark them "migrated" + const migratedNotes = originalNotes.map(note => ({ + text: note.text, + timestamp: "migrated" + })); + + // Store migrated notes in LocalStorage + await page.evaluate((migrated) => { + localStorage.setItem('notes', JSON.stringify(migrated)); + }, migratedNotes); + + // Update metadata to reflect new browser & new notes + metadata.browser = browserName; + metadata.notes = migratedNotes; + + // Update localStorage + await page.evaluate((meta) => { + localStorage.setItem('browserMetadata', JSON.stringify(meta)); + }, metadata); + + // Overwrite the JSON file + fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8'); + + // Double-check that notes are now in localStorage + notesJson = await page.evaluate(() => localStorage.getItem('notes')); + if (!notesJson || notesJson.trim() === "" || notesJson === "null") { + failTest("Migration step failed to populate notes in LocalStorage."); + } + } else { + failTest("No notes found in LocalStorage and no valid cross-browser metadata to migrate."); + } + } else { + failTest("No notes found in LocalStorage and no JSON metadata file available."); + } } + + // At this point, we should have notes in LocalStorage const notes = JSON.parse(notesJson); console.log(`📝 Found ${notes.length} notes:`); for (const note of notes) { console.log(` - ${note.text} at ${note.timestamp}`); } + + // Verify final browser metadata const metadataJson = await page.evaluate(() => localStorage.getItem('browserMetadata')); - if (metadataJson) { - const metadata = JSON.parse(metadataJson); - if (metadata.browser !== browserName) { - failTest(`Expected browser metadata '${browserName}', but got '${metadata.browser}'`); - } - } else { - failTest("No browser metadata found"); + if (!metadataJson) { + failTest("No browser metadata found in LocalStorage at all."); + } + const metadata = JSON.parse(metadataJson); + if (metadata.browser !== browserName) { + failTest(`Expected browser metadata '${browserName}', but got '${metadata.browser}'`); } + console.log(`✅ Verification successful for ${browserName}`); } finally { await context.close(); } + + // Unmount => upload the updated userDataDir to Redis await browserState.unmount(); + process.exit(0); }