"""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="
boom
") b = ErrorRecord(type="exception", message="boom", element_html="
boom
") 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 = "
RuntimeError: boom
" 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 = "
Something went wrong
" 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"]