peregrine/tests/test_e2e_helpers.py
pyr0ball bf1dc39f14 fix(tests): update mock from inner_text() to text_content() in e2e helpers
get_page_errors() was switched to text_content() to capture errors in
CSS-hidden elements (collapsed Streamlit expanders). Two unit test mocks
still stubbed inner_text() — causing CI failures because MagicMock()
returned a non-string from text_content(), breaking the "boom" in message
content assertion.
2026-03-17 20:33:55 -07:00

180 lines
6.4 KiB
Python

"""Unit tests for E2E harness models and helper utilities."""
import fnmatch
import pytest
from unittest.mock import patch, MagicMock
import time
from tests.e2e.models import ErrorRecord, ModeConfig, diff_errors
import tests.e2e.modes.cloud as cloud_mod # imported early so load_dotenv runs before any monkeypatch
def test_error_record_equality():
a = ErrorRecord(type="exception", message="boom", element_html="<div>boom</div>")
b = ErrorRecord(type="exception", message="boom", element_html="<div>boom</div>")
assert a == b
def test_error_record_inequality():
a = ErrorRecord(type="exception", message="boom", element_html="")
b = ErrorRecord(type="alert", message="boom", element_html="")
assert a != b
def test_diff_errors_returns_new_only():
before = [ErrorRecord("exception", "old error", "")]
after = [
ErrorRecord("exception", "old error", ""),
ErrorRecord("alert", "new error", ""),
]
result = diff_errors(before, after)
assert result == [ErrorRecord("alert", "new error", "")]
def test_diff_errors_empty_when_no_change():
errors = [ErrorRecord("exception", "x", "")]
assert diff_errors(errors, errors) == []
def test_diff_errors_empty_before():
after = [ErrorRecord("alert", "boom", "")]
assert diff_errors([], after) == after
def test_mode_config_expected_failure_match():
config = ModeConfig(
name="demo",
base_url="http://localhost:8504",
auth_setup=lambda ctx: None,
expected_failures=["Fetch*", "Generate Cover Letter"],
results_dir=None,
settings_tabs=["👤 My Profile"],
)
assert config.matches_expected_failure("Fetch New Jobs")
assert config.matches_expected_failure("Generate Cover Letter")
assert not config.matches_expected_failure("View Jobs")
def test_mode_config_no_expected_failures():
config = ModeConfig(
name="local",
base_url="http://localhost:8502",
auth_setup=lambda ctx: None,
expected_failures=[],
results_dir=None,
settings_tabs=[],
)
assert not config.matches_expected_failure("Fetch New Jobs")
def test_get_jwt_strategy_b_fallback(monkeypatch):
"""Falls back to persistent JWT when no email env var set."""
monkeypatch.delenv("E2E_DIRECTUS_EMAIL", raising=False)
monkeypatch.setenv("E2E_DIRECTUS_JWT", "persistent.jwt.token")
cloud_mod._token_cache.update({"token": None, "expires_at": 0.0})
assert cloud_mod._get_jwt() == "persistent.jwt.token"
def test_get_jwt_strategy_b_raises_if_no_token(monkeypatch):
"""Raises if neither email nor JWT env var is set."""
monkeypatch.delenv("E2E_DIRECTUS_EMAIL", raising=False)
monkeypatch.delenv("E2E_DIRECTUS_JWT", raising=False)
cloud_mod._token_cache.update({"token": None, "expires_at": 0.0})
with pytest.raises(RuntimeError, match="Cloud mode requires"):
cloud_mod._get_jwt()
def test_get_jwt_strategy_a_login(monkeypatch):
"""Strategy A: calls Directus /auth/login and caches token."""
monkeypatch.setenv("E2E_DIRECTUS_EMAIL", "e2e@circuitforge.tech")
monkeypatch.setenv("E2E_DIRECTUS_PASSWORD", "testpass")
monkeypatch.setenv("E2E_DIRECTUS_URL", "http://fake-directus:8055")
cloud_mod._token_cache.update({"token": None, "expires_at": 0.0})
mock_resp = MagicMock()
mock_resp.json.return_value = {"data": {"access_token": "fresh.jwt", "expires": 900_000}}
mock_resp.raise_for_status = lambda: None
with patch("tests.e2e.modes.cloud.requests.post", return_value=mock_resp) as mock_post:
token = cloud_mod._get_jwt()
assert token == "fresh.jwt"
mock_post.assert_called_once()
assert cloud_mod._token_cache["token"] == "fresh.jwt"
def test_get_jwt_uses_cache(monkeypatch):
"""Returns cached token if not yet expired."""
monkeypatch.setenv("E2E_DIRECTUS_EMAIL", "e2e@circuitforge.tech")
cloud_mod._token_cache.update({"token": "cached.jwt", "expires_at": time.time() + 500})
with patch("tests.e2e.modes.cloud.requests.post") as mock_post:
token = cloud_mod._get_jwt()
assert token == "cached.jwt"
mock_post.assert_not_called()
def test_get_page_errors_finds_exceptions(monkeypatch):
"""get_page_errors returns ErrorRecord for stException elements."""
from tests.e2e.conftest import get_page_errors
mock_el = MagicMock()
mock_el.text_content.return_value = "RuntimeError: boom"
mock_el.inner_html.return_value = "<div>RuntimeError: boom</div>"
mock_page = MagicMock()
mock_page.query_selector_all.side_effect = lambda sel: (
[mock_el] if "stException" in sel else []
)
errors = get_page_errors(mock_page)
assert len(errors) == 1
assert errors[0].type == "exception"
assert "boom" in errors[0].message
def test_get_page_errors_finds_alert_errors(monkeypatch):
"""get_page_errors returns ErrorRecord for stAlert with stAlertContentError child."""
from tests.e2e.conftest import get_page_errors
mock_child = MagicMock()
mock_el = MagicMock()
mock_el.query_selector.return_value = mock_child
mock_el.text_content.return_value = "Something went wrong"
mock_el.inner_html.return_value = "<div>Something went wrong</div>"
mock_page = MagicMock()
mock_page.query_selector_all.side_effect = lambda sel: (
[] if "stException" in sel else [mock_el]
)
errors = get_page_errors(mock_page)
assert len(errors) == 1
assert errors[0].type == "alert"
def test_get_page_errors_ignores_non_error_alerts(monkeypatch):
"""get_page_errors does NOT flag st.warning() or st.info() alerts."""
from tests.e2e.conftest import get_page_errors
mock_el = MagicMock()
mock_el.query_selector.return_value = None
mock_el.inner_text.return_value = "Just a warning"
mock_page = MagicMock()
mock_page.query_selector_all.side_effect = lambda sel: (
[] if "stException" in sel else [mock_el]
)
errors = get_page_errors(mock_page)
assert errors == []
def test_get_console_errors_filters_noise():
"""get_console_errors filters benign Streamlit WebSocket reconnect messages."""
from tests.e2e.conftest import get_console_errors
messages = [
MagicMock(type="error", text="WebSocket connection closed"),
MagicMock(type="error", text="TypeError: cannot read property"),
MagicMock(type="log", text="irrelevant"),
]
errors = get_console_errors(messages)
assert errors == ["TypeError: cannot read property"]