Skip to content

Automatically run guides and gather screenshots of the steps#139

Open
TrentHouliston wants to merge 26 commits intomainfrom
houliston/browser-automation
Open

Automatically run guides and gather screenshots of the steps#139
TrentHouliston wants to merge 26 commits intomainfrom
houliston/browser-automation

Conversation

@TrentHouliston
Copy link
Contributor

Guides can get out of date super easily as things change.

These changes are working towards making sure that any guides we make in the future are self executing and can be self healing with the help of AI MCP rules.

Also it lets us take screenshots of each step so that we can make pretty step by step instructions without having to gather all those screenshots ourselves!

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit

ruff lint

⚠️ [ruff] <E402> reported by reviewdog 🐶
Module level import not at top of file

from tests.guides.sigenergy.run_guide import (
INPUTS_FILE,
SCREENSHOTS_DIR,
run_guide,
)


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f"\n✅ Guide test passed! {len(results)} screenshots captured")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f"📁 Screenshots saved to: {SCREENSHOTS_DIR}")


⚠️ [ruff] <EXE001> reported by reviewdog 🐶
Shebang is present but file is not executable

#!/usr/bin/env python3


⚠️ [ruff] <I001> reported by reviewdog 🐶
Import block is un-sorted or un-formatted

import sys
from pathlib import Path


⚠️ [ruff] <E402> reported by reviewdog 🐶
Module level import not at top of file

from tests.guides.ha_runner import live_home_assistant


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("🚀 Testing in-process Home Assistant startup...")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f"✅ Home Assistant started at {hass.url}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" Port: {hass.port}")


⚠️ [ruff] <PLC0415> reported by reviewdog 🐶
import should be at the top-level of a file

import urllib.request


⚠️ [ruff] <I001> reported by reviewdog 🐶
Import block is un-sorted or un-formatted

import urllib.request
from urllib.error import URLError


⚠️ [ruff] <PLC0415> reported by reviewdog 🐶
import should be at the top-level of a file

from urllib.error import URLError


⚠️ [ruff] <S310> reported by reviewdog 🐶
Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

with urllib.request.urlopen(hass.url, timeout=5) as response:


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" HTTP response: {response.status}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" HTTP error (expected without auth): {e}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("\n📝 Setting entity states...")


⚠️ [ruff] <ANN202> reported by reviewdog 🐶
Missing return type annotation for private function check_states

async def check_states():


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" sensor.test_power: {state1.state if state1 else 'NOT FOUND'}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" sensor.test_energy: {state2.state if state2 else 'NOT FOUND'}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("\n✅ States verified correctly!")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("\n❌ State verification failed!")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("\n🛑 Home Assistant stopped cleanly")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("\n🎭 Testing Playwright integration...")


⚠️ [ruff] <PLC0415> reported by reviewdog 🐶
import should be at the top-level of a file

from playwright.sync_api import sync_playwright


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(" ⚠️ Playwright not installed, skipping browser test")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" HA running at {hass.url}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" Page title: {title}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(" ✅ Playwright can connect to HA!")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" ⚠️ Unexpected title: {title}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("=" * 60)


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("Home Assistant In-Process Runner Tests")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("=" * 60)


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f"\n❌ Basic startup test failed: {e}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f"\n❌ Playwright test failed: {e}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("\n" + "=" * 60)


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("✅ All tests passed!")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("❌ Some tests failed!")

@codecov
Copy link

codecov bot commented Dec 19, 2025

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
1113 2 1111 21
View the top 2 failed test(s) by shortest run time
tests/guides/sigenergy/test_sigenergy_guide.py::test_sigenergy_guide[dark]
Stack Traces | 0.03s run time
dark_mode = True

    @pytest.mark.guide
    @pytest.mark.enable_socket
    @pytest.mark.timeout(300)  # 5 minutes for full guide run
    @pytest.mark.parametrize("dark_mode", [False, True], ids=["light", "dark"])
    def test_sigenergy_guide(dark_mode: bool) -> None:
        """Test the complete Sigenergy setup guide.
    
        This test:
        1. Starts a fresh Home Assistant instance with pre-authenticated user
        2. Loads entity states from scenario1 inputs.json
        3. Runs through all guide steps using Playwright
        4. Captures screenshots at each step
        5. Validates that all elements were created successfully
        """
        # Use separate directories for light and dark mode screenshots
        mode_suffix = "dark" if dark_mode else "light"
        output_dir = SCREENSHOTS_DIR.parent / f"screenshots_{mode_suffix}"
    
        # Clean and create output directory
        if output_dir.exists():
            shutil.rmtree(output_dir)
        output_dir.mkdir(parents=True)
    
>       with live_home_assistant(timeout=120) as hass:
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.../guides/sigenergy/test_sigenergy_guide.py:64: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../...../_temp/uv-python-dir/cpython-3.13.11-linux-x86_64-gnu/lib/python3.13/contextlib.py:141: in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
tests/guides/ha_runner.py:512: in live_home_assistant
    raise error_holder[0]
tests/guides/ha_runner.py:408: in _run
    hass, access_token, refresh_token_id = await _setup_home_assistant_async(port, config_dir)
                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

port = 59261, config_dir = '/tmp/haeo_guide_psdhxdz7'

    async def _setup_home_assistant_async(
        port: int,
        config_dir: str,
    ) -> tuple[HomeAssistant, str, str]:
        """Set up a Home Assistant instance with HTTP server and pre-authenticated user.
    
        This creates a minimal HA instance with just the components needed
        for browser automation: http, frontend, auth, websocket_api.
        Onboarding is bypassed by creating an owner user programmatically.
    
        Returns:
            Tuple of (HomeAssistant instance, access_token, refresh_token_id)
    
        """
        # Pre-populate onboarding storage to mark all steps complete
        # This MUST be done BEFORE the HomeAssistant instance is created,
        # because the StoreManager scans the storage directory during initialization
        # and caches which files exist. If we write the file after that scan,
        # the onboarding component won't see it.
        storage_dir = Path(config_dir) / ".storage"
        storage_dir.mkdir(exist_ok=True)
        onboarding_storage = storage_dir / "onboarding"
        onboarding_data = {
            "version": 4,
            "minor_version": 1,
            "key": "onboarding",
            "data": {"done": ["user", "core_config", "analytics", "integration"]},
        }
        onboarding_storage.write_text(json.dumps(onboarding_data))
    
        hass = HomeAssistant(config_dir)
    
        # Basic configuration
        hass.config.location_name = "Test Home"
        hass.config.latitude = 32.87336
        hass.config.longitude = -117.22743
        hass.config.elevation = 0
        await hass.config.async_set_time_zone("UTC")
        hass.config.skip_pip = True
        hass.config.skip_pip_packages = []
    
        # Set up loader - don't set DATA_CUSTOM_COMPONENTS, let loader discover them
        loader.async_setup(hass)
    
        # Set up config entries
        hass.config_entries = ConfigEntries(hass, {"_": "placeholder"})
        hass.bus.async_listen_once(
            EVENT_HOMEASSISTANT_STOP,
            hass.config_entries._async_shutdown,
        )
    
        # Set up essential helpers
        entity.async_setup(hass)
    
        # Translation cache
        hass.data[translation.TRANSLATION_FLATTEN_CACHE] = translation._TranslationCache(hass)
    
        # Load registries
        await ar.async_load(hass)
        await cr.async_load(hass)
        await dr.async_load(hass)
        await er.async_load(hass)
        await fr.async_load(hass)
        await ir.async_load(hass)
        await lr.async_load(hass)
        await rs.async_load(hass)
    
        # Set up auth with homeassistant provider
        hass.auth = await auth_manager_from_config(
            hass,
            provider_configs=[{"type": "homeassistant"}],
            module_configs=[],
        )
    
        # Get the homeassistant auth provider to add a user with password.
        # We configure the provider as "homeassistant" type above, so index 0 is always HassAuthProvider.
        # Pyright can't narrow AuthProvider to HassAuthProvider due to incomplete type stubs
        # in Home Assistant - async_add_auth exists on HassAuthProvider but not AuthProvider.
        provider = hass.auth.auth_providers[0]
        await provider.async_add_auth("testuser", "testpass")  # pyright: ignore[reportAttributeAccessIssue]
    
        # Create owner user to bypass onboarding
        # First non-system user automatically becomes owner
        owner = await hass.auth.async_create_user(
            name="Test User",
            group_ids=["system-admin"],
        )
    
        # Create credential and link to user
        credential = Credentials(
            id="test-credential",
            auth_provider_type="homeassistant",
            auth_provider_id=None,
            data={"username": "testuser"},
            is_new=False,
        )
        await hass.auth.async_link_user(owner, credential)
    
        # Create refresh token and access token
        refresh_token = await hass.auth.async_create_refresh_token(
            owner,
            CLIENT_ID,
            credential=credential,
        )
        access_token = hass.auth.async_create_access_token(refresh_token)
        # Store refresh token ID for frontend auth
        refresh_token_id = refresh_token.id
    
        # Set up HTTP on ephemeral port
        http_config = {
            "server_port": port,
        }
    
        # Suppress aiohttp.web_exceptions.NotAppKeyWarning which is raised as an error
        # in newer versions of aiohttp when HA sets app["hass"] = hass
        # This is a compatibility issue between HA and newer aiohttp versions
        warnings.filterwarnings("ignore", category=DeprecationWarning, module="aiohttp")
        try:
            # NotAppKeyWarning only exists in newer aiohttp versions
            from aiohttp.web_exceptions import NotAppKeyWarning  # noqa: PLC0415
    
            warnings.filterwarnings("ignore", category=NotAppKeyWarning)
        except ImportError:
            pass  # Older aiohttp doesn't have this
    
        # Set up components in order (onboarding will see all steps done and skip
        # because we pre-populated the storage file)
        assert await async_setup_component(hass, "http", {"http": http_config})
        assert await async_setup_component(hass, "websocket_api", {})
        assert await async_setup_component(hass, "auth", {})
        assert await async_setup_component(hass, "onboarding", {})
    
        # Verify onboarding is bypassed
        # Import here because component must be set up first
        from homeassistant.components.onboarding import async_is_onboarded  # noqa: PLC0415
    
        if not async_is_onboarded(hass):
            msg = "Onboarding bypass failed - check storage file format and timing"
            raise RuntimeError(msg)
    
>       assert await async_setup_component(hass, "frontend", {})
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       AssertionError

tests/guides/ha_runner.py:375: AssertionError
tests/guides/sigenergy/test_sigenergy_guide.py::test_sigenergy_guide[light]
Stack Traces | 0.049s run time
dark_mode = False

    @pytest.mark.guide
    @pytest.mark.enable_socket
    @pytest.mark.timeout(300)  # 5 minutes for full guide run
    @pytest.mark.parametrize("dark_mode", [False, True], ids=["light", "dark"])
    def test_sigenergy_guide(dark_mode: bool) -> None:
        """Test the complete Sigenergy setup guide.
    
        This test:
        1. Starts a fresh Home Assistant instance with pre-authenticated user
        2. Loads entity states from scenario1 inputs.json
        3. Runs through all guide steps using Playwright
        4. Captures screenshots at each step
        5. Validates that all elements were created successfully
        """
        # Use separate directories for light and dark mode screenshots
        mode_suffix = "dark" if dark_mode else "light"
        output_dir = SCREENSHOTS_DIR.parent / f"screenshots_{mode_suffix}"
    
        # Clean and create output directory
        if output_dir.exists():
            shutil.rmtree(output_dir)
        output_dir.mkdir(parents=True)
    
>       with live_home_assistant(timeout=120) as hass:
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.../guides/sigenergy/test_sigenergy_guide.py:64: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../...../_temp/uv-python-dir/cpython-3.13.11-linux-x86_64-gnu/lib/python3.13/contextlib.py:141: in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
tests/guides/ha_runner.py:512: in live_home_assistant
    raise error_holder[0]
tests/guides/ha_runner.py:408: in _run
    hass, access_token, refresh_token_id = await _setup_home_assistant_async(port, config_dir)
                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

port = 51609, config_dir = '/tmp/haeo_guide_9pzbu9we'

    async def _setup_home_assistant_async(
        port: int,
        config_dir: str,
    ) -> tuple[HomeAssistant, str, str]:
        """Set up a Home Assistant instance with HTTP server and pre-authenticated user.
    
        This creates a minimal HA instance with just the components needed
        for browser automation: http, frontend, auth, websocket_api.
        Onboarding is bypassed by creating an owner user programmatically.
    
        Returns:
            Tuple of (HomeAssistant instance, access_token, refresh_token_id)
    
        """
        # Pre-populate onboarding storage to mark all steps complete
        # This MUST be done BEFORE the HomeAssistant instance is created,
        # because the StoreManager scans the storage directory during initialization
        # and caches which files exist. If we write the file after that scan,
        # the onboarding component won't see it.
        storage_dir = Path(config_dir) / ".storage"
        storage_dir.mkdir(exist_ok=True)
        onboarding_storage = storage_dir / "onboarding"
        onboarding_data = {
            "version": 4,
            "minor_version": 1,
            "key": "onboarding",
            "data": {"done": ["user", "core_config", "analytics", "integration"]},
        }
        onboarding_storage.write_text(json.dumps(onboarding_data))
    
        hass = HomeAssistant(config_dir)
    
        # Basic configuration
        hass.config.location_name = "Test Home"
        hass.config.latitude = 32.87336
        hass.config.longitude = -117.22743
        hass.config.elevation = 0
        await hass.config.async_set_time_zone("UTC")
        hass.config.skip_pip = True
        hass.config.skip_pip_packages = []
    
        # Set up loader - don't set DATA_CUSTOM_COMPONENTS, let loader discover them
        loader.async_setup(hass)
    
        # Set up config entries
        hass.config_entries = ConfigEntries(hass, {"_": "placeholder"})
        hass.bus.async_listen_once(
            EVENT_HOMEASSISTANT_STOP,
            hass.config_entries._async_shutdown,
        )
    
        # Set up essential helpers
        entity.async_setup(hass)
    
        # Translation cache
        hass.data[translation.TRANSLATION_FLATTEN_CACHE] = translation._TranslationCache(hass)
    
        # Load registries
        await ar.async_load(hass)
        await cr.async_load(hass)
        await dr.async_load(hass)
        await er.async_load(hass)
        await fr.async_load(hass)
        await ir.async_load(hass)
        await lr.async_load(hass)
        await rs.async_load(hass)
    
        # Set up auth with homeassistant provider
        hass.auth = await auth_manager_from_config(
            hass,
            provider_configs=[{"type": "homeassistant"}],
            module_configs=[],
        )
    
        # Get the homeassistant auth provider to add a user with password.
        # We configure the provider as "homeassistant" type above, so index 0 is always HassAuthProvider.
        # Pyright can't narrow AuthProvider to HassAuthProvider due to incomplete type stubs
        # in Home Assistant - async_add_auth exists on HassAuthProvider but not AuthProvider.
        provider = hass.auth.auth_providers[0]
        await provider.async_add_auth("testuser", "testpass")  # pyright: ignore[reportAttributeAccessIssue]
    
        # Create owner user to bypass onboarding
        # First non-system user automatically becomes owner
        owner = await hass.auth.async_create_user(
            name="Test User",
            group_ids=["system-admin"],
        )
    
        # Create credential and link to user
        credential = Credentials(
            id="test-credential",
            auth_provider_type="homeassistant",
            auth_provider_id=None,
            data={"username": "testuser"},
            is_new=False,
        )
        await hass.auth.async_link_user(owner, credential)
    
        # Create refresh token and access token
        refresh_token = await hass.auth.async_create_refresh_token(
            owner,
            CLIENT_ID,
            credential=credential,
        )
        access_token = hass.auth.async_create_access_token(refresh_token)
        # Store refresh token ID for frontend auth
        refresh_token_id = refresh_token.id
    
        # Set up HTTP on ephemeral port
        http_config = {
            "server_port": port,
        }
    
        # Suppress aiohttp.web_exceptions.NotAppKeyWarning which is raised as an error
        # in newer versions of aiohttp when HA sets app["hass"] = hass
        # This is a compatibility issue between HA and newer aiohttp versions
        warnings.filterwarnings("ignore", category=DeprecationWarning, module="aiohttp")
        try:
            # NotAppKeyWarning only exists in newer aiohttp versions
            from aiohttp.web_exceptions import NotAppKeyWarning  # noqa: PLC0415
    
            warnings.filterwarnings("ignore", category=NotAppKeyWarning)
        except ImportError:
            pass  # Older aiohttp doesn't have this
    
        # Set up components in order (onboarding will see all steps done and skip
        # because we pre-populated the storage file)
        assert await async_setup_component(hass, "http", {"http": http_config})
        assert await async_setup_component(hass, "websocket_api", {})
        assert await async_setup_component(hass, "auth", {})
        assert await async_setup_component(hass, "onboarding", {})
    
        # Verify onboarding is bypassed
        # Import here because component must be set up first
        from homeassistant.components.onboarding import async_is_onboarded  # noqa: PLC0415
    
        if not async_is_onboarded(hass):
            msg = "Onboarding bypass failed - check storage file format and timing"
            raise RuntimeError(msg)
    
>       assert await async_setup_component(hass, "frontend", {})
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       AssertionError

tests/guides/ha_runner.py:375: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

- Delete singlefile_capture.py (SingleFile JS injection for HTML snapshots)
- Remove single-file-cli from package.json devDependencies
- Strip HTML capture code from run_guide.py
- Keep PNG screenshot capture using Playwright's native screenshot()
- Add .gitignore for guide test outputs (logs, screenshots)
Resolved conflicts:
- pyproject.toml: Keep both playwright and networkx dependencies
- docs/user-guide/examples/sigenergy-system.md: Accept main version (better formatting)
- uv.lock: Regenerated with all dependencies
- Add dark mode parameter to test and run_guide
- Update ha_runner inject_auth to set theme preference
- Add aiohttp NotAppKeyWarning suppression for compatibility
- Update field labels to match current translations (en.json):
  - Hub: 'System Name' instead of 'Network Name'
  - Integration search uses 'HAEO'
  - Battery/Solar/Grid/Load field labels aligned with translations
- Fix two-step config flows:
  - Grid: submit step 1 for entities, step 2 for limits
  - Load: use 'HAEO Configurable' entity and step 2 for constant value
  - Battery: add wait and check for step 2 values form
- Fix close_element_dialog to try multiple button names and wait properly
- Add LONG_WAIT after submit for async processing
- Use outline + box-shadow for element highlighting (not clipped by Shadow DOM)
- Add z-index and position adjustments for visibility above siblings
- Handle ha-integration-list-item, role=listitem, role=option elements
- Add capture_name support to add_another_entity() function
- Skip fill_textbox when field already contains target value
- Capture all solar forecast selections (4 total)
- Capture all grid price entity selections (2 import, 2 export)

Addresses feedback:
- Screenshots now captured for all entity selections
- 'Add entity' button clicks are now captured
- Highlighting surrounds elements properly via outline
- Pre-filled name fields are skipped
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit

ruff lint

⚠️ [ruff] <PLC0415> reported by reviewdog 🐶
import should be at the top-level of a file

from urllib.error import URLError


⚠️ [ruff] <S310> reported by reviewdog 🐶
Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

with urllib.request.urlopen(hass.url, timeout=5) as response:


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" HTTP response: {response.status}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" HTTP error (expected without auth): {e}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("\n📝 Setting entity states...")


⚠️ [ruff] <ANN202> reported by reviewdog 🐶
Missing return type annotation for private function check_states

async def check_states():


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" sensor.test_power: {state1.state if state1 else 'NOT FOUND'}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" sensor.test_energy: {state2.state if state2 else 'NOT FOUND'}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("\n✅ States verified correctly!")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("\n❌ State verification failed!")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("\n🛑 Home Assistant stopped cleanly")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("\n🎭 Testing Playwright integration...")


⚠️ [ruff] <PLC0415> reported by reviewdog 🐶
import should be at the top-level of a file

from playwright.sync_api import sync_playwright


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(" ⚠️ Playwright not installed, skipping browser test")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" HA running at {hass.url}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" Page title: {title}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(" ✅ Playwright can connect to HA!")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f" ⚠️ Unexpected title: {title}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("=" * 60)


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("Home Assistant In-Process Runner Tests")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("=" * 60)


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f"\n❌ Basic startup test failed: {e}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print(f"\n❌ Playwright test failed: {e}")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("\n" + "=" * 60)


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("✅ All tests passed!")


⚠️ [ruff] <T201> reported by reviewdog 🐶
print found

print("❌ Some tests failed!")

- Replace inline element styling with overlay positioned at bounding box
- Use popover API for top-layer placement (avoids parent overflow clipping)
- Focus elements before highlighting to update UI state
- Target label.mdc-text-field for text input highlighting
- Target .combo-box-row for entity picker item highlighting
- Capture element border-radius for rounded overlay corners
- Remove unused _ensure_click_indicator_styles and _CLICK_INDICATOR_STYLE
- Remove debug_ha.py (standalone debug script)
- Remove test_ha_runner.py (manual test script not needed in CI)
- Add .ruff.toml to ignore intentional patterns (late imports, etc.)
- Fix type annotations in conftest.py, ha_runner.py, test_sigenergy_guide.py
- Replace print statements with logging
- Format all files with ruff
- Extract JavaScript click indicator to tests/guides/js/click_indicator.js
  for better maintainability

- Remove blanket .ruff.toml ignore file; add inline noqa comments with
  explanations for intentional late imports (PLC0415)

- Convert asyncio.sleep loop pattern to proper asyncio.Event signaling:
  - Use asyncio.Event instead of threading.Event for stop signal
  - Signal via loop.call_soon_threadsafe() for thread-safe shutdown

- Remove sys.path manipulation from run_guide.py and test_sigenergy_guide.py;
  imports work correctly without it using fully qualified paths

- Add pyright:ignore[reportAttributeAccessIssue] for async_add_auth call
  where Home Assistant type stubs don't include the subclass method
Two-layer architecture:
- HAPage: Low-level HA UI primitives (may change between HA versions)
- HAEO primitives: High-level element functions accepting schema TypedDicts

Features:
- Screenshot capture with click indicators
- Form interactions (textbox, spinbutton, combobox)
- Entity picker dialog handling
- Integration search and setup
- Add guide.py using HAPage primitives (~370 lines vs ~1000)
- Delete run_guide.py (superseded by guide.py)
- Update test import to use new guide module

Keeps Sigenergy-specific entity names and search terms in guide.py
while delegating HA UI interactions to the primitives package.
JavaScript for click indicator overlay is now inlined as _CLICK_INDICATOR_JS
constant to avoid file loading issues and keep related code together.
- haeo.py: Add typed config dataclasses (InverterConfig, BatteryConfig, etc.)
  with Entity tuples for (search_term, display_name)
- guide.py: Now just defines Sigenergy config data and calls primitives
  (~170 lines vs ~370 lines)
- Primitives handle all HA UI interactions
- Guide just specifies what to configure, not how
- Add ScreenshotContext with OrderedDict and stack-based naming
- Add @guide_step decorator for automatic scope management
- Move click indicator JS to external file (primitives/js/)
- Always capture screenshots when context is active
- HAPage methods use context.scope() for hierarchical names
- HAEO primitives all decorated with @guide_step
- Guide.py uses inline arguments for walkthrough style
- Screenshots named like: add_grid.entity_Import_Price.search_results
- Rename screenshots.py to capture.py (avoid gitignore pattern)
Comment on lines +37 to +38
"ha-list-item, mwc-list-item, md-list-item, " +
'[role="listitem"], [role="option"]'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[prettier] reported by reviewdog 🐶

Suggested change
"ha-list-item, mwc-list-item, md-list-item, " +
'[role="listitem"], [role="option"]'
"ha-list-item, mwc-list-item, md-list-item, " + '[role="listitem"], [role="option"]'

Comment on lines +41 to +43
const listItemParent = entityListItem.closest(
"ha-list-item, mwc-list-item, md-list-item, md-item"
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[prettier] reported by reviewdog 🐶

Suggested change
const listItemParent = entityListItem.closest(
"ha-list-item, mwc-list-item, md-list-item, md-item"
);
const listItemParent = entityListItem.closest("ha-list-item, mwc-list-item, md-list-item, md-item");

Comment on lines +58 to +60
const haWrapper = roleItem.closest(
"ha-list-item, md-item, mwc-list-item, .combo-box-row"
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[prettier] reported by reviewdog 🐶

Suggested change
const haWrapper = roleItem.closest(
"ha-list-item, md-item, mwc-list-item, .combo-box-row"
);
const haWrapper = roleItem.closest("ha-list-item, md-item, mwc-list-item, .combo-box-row");

Comment on lines +20 to +39
from tests.guides.primitives.ha_page import HAPage
from tests.guides.primitives.haeo import (
Entity,
add_battery,
add_grid,
add_integration,
add_inverter,
add_load,
add_node,
add_solar,
login,
verify_setup,
)
from tests.guides.primitives.capture import (
ScreenshotContext,
guide_step,
screenshot_context,
)

__all__ = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] <I001> reported by reviewdog 🐶
Import block is un-sorted or un-formatted

Suggested change
from tests.guides.primitives.ha_page import HAPage
from tests.guides.primitives.haeo import (
Entity,
add_battery,
add_grid,
add_integration,
add_inverter,
add_load,
add_node,
add_solar,
login,
verify_setup,
)
from tests.guides.primitives.capture import (
ScreenshotContext,
guide_step,
screenshot_context,
)
__all__ = [
from tests.guides.primitives.capture import ScreenshotContext, guide_step, screenshot_context
from tests.guides.primitives.ha_page import HAPage
from tests.guides.primitives.haeo import (
Entity,
add_battery,
add_grid,
add_integration,
add_inverter,
add_load,
add_node,
add_solar,
login,
verify_setup,
)
__all__ = [

Comment on lines +39 to +58
__all__ = [
# Screenshot context
"ScreenshotContext",
"guide_step",
"screenshot_context",
# Low-level primitives
"HAPage",
# Entity type
"Entity",
# HAEO element primitives
"add_battery",
"add_grid",
"add_integration",
"add_inverter",
"add_load",
"add_node",
"add_solar",
"login",
"verify_setup",
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] <RUF022> reported by reviewdog 🐶
__all__ is not sorted

Suggested change
__all__ = [
# Screenshot context
"ScreenshotContext",
"guide_step",
"screenshot_context",
# Low-level primitives
"HAPage",
# Entity type
"Entity",
# HAEO element primitives
"add_battery",
"add_grid",
"add_integration",
"add_inverter",
"add_load",
"add_node",
"add_solar",
"login",
"verify_setup",
]
__all__ = [
# Entity type
"Entity",
# Low-level primitives
"HAPage",
# Screenshot context
"ScreenshotContext",
# HAEO element primitives
"add_battery",
"add_grid",
"add_integration",
"add_inverter",
"add_load",
"add_node",
"add_solar",
"guide_step",
"login",
"screenshot_context",
"verify_setup",
]

from dataclasses import dataclass, field
from functools import wraps
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, TypeVar
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] <UP035> reported by reviewdog 🐶
Import from collections.abc instead: Callable

Suggested change
from typing import TYPE_CHECKING, Any, Callable, TypeVar
from typing import TYPE_CHECKING, Any, TypeVar
from collections.abc import Callable

# ctx.screenshots contains all captured screenshots

"""
global _current_context
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] <PLW0603> reported by reviewdog 🐶
Using the global statement to update _current_context is discouraged

# ctx.screenshots contains all captured screenshots

"""
global _current_context
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] <PLW0603> reported by reviewdog 🐶
Using the global statement to update _current_context is discouraged

_current_context = previous


def guide_step(func: F) -> F:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] <UP047> reported by reviewdog 🐶
Generic function guide_step should use type parameters

Suggested change
def guide_step(func: F) -> F:
def guide_step[F: Callable[..., Any]](func: F) -> F:

Comment on lines +140 to +151
"""Decorator for HAEO primitive functions.

Wraps the function to push its name onto the screenshot context stack,
so all screenshots taken within the function get hierarchical names.

Usage:
@guide_step
def add_battery(page: HAPage, name: str, ...):
page.fill_textbox("Battery Name", name) # → "add_battery.fill_textbox.Battery_Name"
...

"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] <D401> reported by reviewdog 🐶
First line of docstring should be in imperative mood: "Decorator for HAEO primitive functions."

Comment on lines +13 to +38
from __future__ import annotations

import logging
from collections import OrderedDict
from pathlib import Path
import shutil
import sys
from typing import Any

from playwright.sync_api import sync_playwright

from tests.guides.ha_runner import LiveHomeAssistant, live_home_assistant
from tests.guides.primitives import (
HAPage,
add_battery,
add_grid,
add_integration,
add_inverter,
add_load,
add_solar,
login,
screenshot_context,
verify_setup,
)

_LOGGER = logging.getLogger(__name__)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] <I001> reported by reviewdog 🐶
Import block is un-sorted or un-formatted

Suggested change
from __future__ import annotations
import logging
from collections import OrderedDict
from pathlib import Path
import shutil
import sys
from typing import Any
from playwright.sync_api import sync_playwright
from tests.guides.ha_runner import LiveHomeAssistant, live_home_assistant
from tests.guides.primitives import (
HAPage,
add_battery,
add_grid,
add_integration,
add_inverter,
add_load,
add_solar,
login,
screenshot_context,
verify_setup,
)
_LOGGER = logging.getLogger(__name__)
from __future__ import annotations
from collections import OrderedDict
import logging
from pathlib import Path
import shutil
import sys
from typing import Any
from playwright.sync_api import sync_playwright
from tests.guides.ha_runner import LiveHomeAssistant, live_home_assistant
from tests.guides.primitives import (
HAPage,
add_battery,
add_grid,
add_integration,
add_inverter,
add_load,
add_solar,
login,
screenshot_context,
verify_setup,
)
_LOGGER = logging.getLogger(__name__)

Comment on lines +20 to +21
from typing import Any

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] <F401> reported by reviewdog 🐶
typing.Any imported but unused

Suggested change
from typing import Any

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant