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.
180 lines
6.4 KiB
Python
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"]
|