Skip to content

Commit 454b68c

Browse files
test: replace monolithic tests with structured unit and e2e suite
Remove old test_member_app.py and test_utils.py. Add structured test suite under tests/: - conftest.py: shared pytest fixtures - e2e/test_app_e2e.py: end-to-end smoke test for app launch - unit/components/: tests for FormControl, AliasEntry, SocialEntry, Layout - unit/screens/: tests for all 8 screens (auth, dashboard, language, loading, member_form, member_list, quit_confirm, save_loading) - unit/test_*.py: unit tests for app, file_io, git_client, github_client, main, markdown_builder, strings, __version__ Coverage: 97% across the full src/ package.
1 parent 51d3b3f commit 454b68c

27 files changed

Lines changed: 1504 additions & 775 deletions

tests/conftest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import gettext
2+
from unittest.mock import MagicMock
3+
4+
import pytest
5+
6+
7+
class DummyTranslation:
8+
def gettext(self, message):
9+
return message
10+
11+
def install(self):
12+
import builtins
13+
14+
builtins._ = self.gettext # ty:ignore[unresolved-attribute]
15+
16+
17+
@pytest.fixture(autouse=True)
18+
def mock_gettext(monkeypatch):
19+
monkeypatch.setattr(
20+
gettext, "translation", lambda *args, **kwargs: DummyTranslation()
21+
)
22+
23+
24+
@pytest.fixture
25+
def mock_github_auth(monkeypatch):
26+
monkeypatch.setattr("edit_python_pe.github_client.Github", MagicMock())
27+
monkeypatch.setattr(
28+
"edit_python_pe.github_client.get_repo",
29+
MagicMock(return_value=("fake-token", MagicMock())),
30+
)

tests/e2e/test_app_e2e.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import tempfile
2+
from unittest.mock import MagicMock, patch
3+
4+
import pytest
5+
6+
from edit_python_pe.app import MemberApp
7+
from edit_python_pe.screens.auth import AuthScreen
8+
from edit_python_pe.screens.dashboard import DashboardScreen
9+
from edit_python_pe.screens.language import LanguageScreen
10+
from edit_python_pe.screens.member_form import MemberFormScreen
11+
from edit_python_pe.screens.save_loading import SaveLoadingScreen
12+
13+
14+
class TestAppE2E:
15+
@pytest.mark.asyncio
16+
@patch("edit_python_pe.screens.loading.get_repo")
17+
@patch("edit_python_pe.screens.loading.fork_repo")
18+
@patch("edit_python_pe.github_client._commit_and_push")
19+
@patch("keyring.get_password", return_value=None)
20+
@patch("keyring.set_password")
21+
async def test_full_app_flow(
22+
self,
23+
mock_set_password,
24+
mock_get_password,
25+
mock_commit_and_push,
26+
mock_fork_repo,
27+
mock_get_repo_loading,
28+
):
29+
mock_repo = MagicMock()
30+
mock_forked = MagicMock()
31+
mock_get_repo_loading.return_value = ("fake-token", mock_repo)
32+
mock_fork_repo.return_value = (tempfile.gettempdir(), mock_forked)
33+
mock_commit_and_push.return_value = (
34+
"Commit message",
35+
MagicMock(),
36+
MagicMock(),
37+
MagicMock(),
38+
)
39+
40+
# Create a mock PR
41+
mock_pr = MagicMock()
42+
mock_pr.html_url = "https://github.com/fake/pr"
43+
mock_repo.get_pulls.return_value = []
44+
mock_repo.create_pull.return_value = mock_pr
45+
46+
app = MemberApp()
47+
async with app.run_test(size=(120, 100)) as pilot:
48+
# 1. Language Screen
49+
assert isinstance(app.screen, LanguageScreen)
50+
await pilot.click("#lang-continue")
51+
await pilot.pause()
52+
53+
# 2. Auth Screen
54+
assert isinstance(app.screen, AuthScreen)
55+
await pilot.click("#github-token")
56+
await pilot.press("f", "a", "k", "e")
57+
await pilot.pause()
58+
await pilot.click("#login-btn")
59+
60+
# 3. Loading Screen
61+
# Wait for background task to finish and transition to Dashboard
62+
for _ in range(20):
63+
await pilot.pause(0.1)
64+
if isinstance(app.screen, DashboardScreen):
65+
break
66+
67+
for _ in range(10):
68+
await pilot.pause(0.1)
69+
if isinstance(app.screen, DashboardScreen):
70+
break
71+
assert isinstance(app.screen, DashboardScreen)
72+
73+
# 4. Dashboard -> Add Member
74+
await pilot.click("#dash-add")
75+
await pilot.pause()
76+
assert isinstance(app.screen, MemberFormScreen)
77+
78+
# 5.1 Validation test
79+
await pilot.click("#member-form-add-alias")
80+
await pilot.click("#member-form-add-social")
81+
82+
# Fill with invalid URLs
83+
app.screen.homepage_input.value = "not_a_url"
84+
85+
# Since social entries are dynamically added, get the last one
86+
if app.screen.social_entries:
87+
app.screen.social_entries[-1].url_input.value = "not_a_social_url"
88+
89+
await pilot.click("#member-form-save")
90+
await pilot.pause()
91+
92+
# Validation should prevent navigating away
93+
assert isinstance(
94+
app.screen,
95+
__import__(
96+
"edit_python_pe.screens.member_form"
97+
).screens.member_form.MemberFormScreen,
98+
)
99+
100+
# 5. Member Form Screen
101+
app.screen.name_input.value = "John Doe"
102+
app.screen.email_input.value = "john@example.com"
103+
app.screen.city_input.value = "Lima"
104+
app.screen.homepage_input.value = "https://example.com"
105+
if app.screen.social_entries:
106+
app.screen.social_entries[
107+
-1
108+
].url_input.value = "https://github.com/john"
109+
110+
await pilot.click("#member-form-save")
111+
112+
# 6. Save Loading Screen
113+
for _ in range(40):
114+
await pilot.pause(0.1)
115+
if (
116+
isinstance(app.screen, SaveLoadingScreen)
117+
and app.screen.query("#loading-actions")
118+
and app.screen.query("#loading-actions").first().display
119+
):
120+
break
121+
122+
assert isinstance(app.screen, SaveLoadingScreen)
123+
assert "was saved successfully" in str(
124+
app.screen.query_one("#result-msg").render()
125+
)
126+
127+
# 7. Back to Dashboard
128+
app.pop_screen()
129+
await pilot.pause()
130+
131+
for _ in range(10):
132+
await pilot.pause(0.1)
133+
if isinstance(app.screen, MemberFormScreen):
134+
break
135+
136+
# Now we are back at MemberFormScreen
137+
assert isinstance(app.screen, MemberFormScreen)
138+
139+
# Click discard to go back to Dashboard
140+
app.pop_screen()
141+
await pilot.pause()
142+
for _ in range(10):
143+
await pilot.pause(0.1)
144+
if isinstance(app.screen, DashboardScreen):
145+
break
146+
assert isinstance(app.screen, DashboardScreen)

0 commit comments

Comments
 (0)