gabriel / musehub public
test_musehub_ui_forks_ssr.py python
186 lines 6.3 KB
c0f0b481 release: merge dev → main (#5) Gabriel Cardona <cgcardona@gmail.com> 5d ago
1 """SSR tests for the MuseHub fork network page (issue #561).
2
3 Verifies that the fork table is rendered server-side (HTML in the initial
4 response body) and that the SVG DAG container and window.__forkNetwork data
5 are present for the JavaScript renderer.
6
7 Covers:
8 - test_forks_page_renders_fork_owner_server_side — fork owner appears in SSR HTML
9 - test_forks_page_shows_total_count — total_forks badge in SSR HTML
10 - test_forks_page_empty_state_when_no_forks — no forks -> empty-state message
11 - test_forks_page_dag_container_present — fork-dag-container div present for JS
12 - test_forks_page_fork_network_json_in_html — window.__forkNetwork injected for DAG JS
13 """
14 from __future__ import annotations
15
16 import pytest
17 from httpx import AsyncClient
18 from sqlalchemy.ext.asyncio import AsyncSession
19
20 from musehub.db.musehub_models import MusehubFork, MusehubRepo
21
22
23 # ---------------------------------------------------------------------------
24 # Helpers
25 # ---------------------------------------------------------------------------
26
27
28 async def _make_repo(
29 db: AsyncSession,
30 owner: str = "upstream",
31 slug: str = "bass-project",
32 visibility: str = "public",
33 ) -> str:
34 """Seed a public repo and return its repo_id string."""
35 repo = MusehubRepo(
36 name=slug,
37 owner=owner,
38 slug=slug,
39 visibility=visibility,
40 owner_user_id=f"uid-{owner}",
41 )
42 db.add(repo)
43 await db.commit()
44 await db.refresh(repo)
45 return str(repo.repo_id)
46
47
48 async def _make_fork(
49 db: AsyncSession,
50 source_repo_id: str,
51 fork_owner: str = "forker",
52 fork_slug: str = "bass-project",
53 ) -> str:
54 """Seed a fork repo + fork relationship; return the fork repo_id."""
55 fork_repo = MusehubRepo(
56 name=fork_slug,
57 owner=fork_owner,
58 slug=fork_slug,
59 visibility="public",
60 owner_user_id=f"uid-{fork_owner}",
61 description=f"Fork of upstream/{fork_slug}",
62 )
63 db.add(fork_repo)
64 await db.commit()
65 await db.refresh(fork_repo)
66
67 fork_record = MusehubFork(
68 source_repo_id=source_repo_id,
69 fork_repo_id=str(fork_repo.repo_id),
70 forked_by=fork_owner,
71 )
72 db.add(fork_record)
73 await db.commit()
74 return str(fork_repo.repo_id)
75
76
77 # ---------------------------------------------------------------------------
78 # SSR tests
79 # ---------------------------------------------------------------------------
80
81
82 @pytest.mark.anyio
83 async def test_forks_page_renders_fork_owner_server_side(
84 client: AsyncClient,
85 db_session: AsyncSession,
86 ) -> None:
87 """Fork owner appears in the initial HTML response -- server-rendered, not JS-injected.
88
89 The table row for each fork must be present in the HTML body returned by the
90 server, before any client-side JavaScript runs. This is the primary SSR
91 contract: crawlers and non-JS clients can see fork data.
92 """
93 source_id = await _make_repo(db_session)
94 await _make_fork(db_session, source_id, fork_owner="alice", fork_slug="bass-project")
95
96 response = await client.get("/upstream/bass-project/forks")
97 assert response.status_code == 200
98 body = response.text
99 # Fork owner must appear in server-rendered HTML (table row), not just in JS data
100 assert "alice" in body
101 # The fork link href must be a server-rendered anchor tag
102 assert "/alice/bass-project" in body
103
104
105 @pytest.mark.anyio
106 async def test_forks_page_shows_total_count(
107 client: AsyncClient,
108 db_session: AsyncSession,
109 ) -> None:
110 """The total_forks count is present in the server-rendered HTML.
111
112 The count badge must appear in static HTML so users and crawlers see the
113 correct count without executing JavaScript.
114 """
115 source_id = await _make_repo(db_session)
116 await _make_fork(db_session, source_id, fork_owner="bob", fork_slug="bass-project")
117 await _make_fork(db_session, source_id, fork_owner="carol", fork_slug="bass-project")
118
119 response = await client.get("/upstream/bass-project/forks")
120 assert response.status_code == 200
121 body = response.text
122 # Both the count and the word "fork" must appear in the SSR page
123 assert "2" in body
124 assert "fork" in body.lower()
125
126
127 @pytest.mark.anyio
128 async def test_forks_page_empty_state_when_no_forks(
129 client: AsyncClient,
130 db_session: AsyncSession,
131 ) -> None:
132 """A repo with zero forks renders an empty-state message instead of a table.
133
134 The server must not render an empty table -- it should display a human-readable
135 message so users understand there are no forks yet.
136 """
137 await _make_repo(db_session)
138
139 response = await client.get("/upstream/bass-project/forks")
140 assert response.status_code == 200
141 body = response.text
142 # Empty-state copy must be present server-side
143 assert "No forks yet" in body
144 # No table rows for fork data
145 assert "<tbody>" not in body
146
147
148 @pytest.mark.anyio
149 async def test_forks_page_dag_container_present(
150 client: AsyncClient,
151 db_session: AsyncSession,
152 ) -> None:
153 """The SVG DAG container element is present in the server-rendered HTML.
154
155 The complex layout algorithm stays as JavaScript, but the host container
156 element must exist in the static HTML so the JS renderer can mount into it
157 without a race condition or missing-element error.
158 """
159 await _make_repo(db_session)
160
161 response = await client.get("/upstream/bass-project/forks")
162 assert response.status_code == 200
163 body = response.text
164 # Either the named container or the SVG element itself must be present
165 assert "fork-dag-container" in body or "fork-svg" in body
166
167
168 @pytest.mark.anyio
169 async def test_forks_page_fork_network_json_in_html(
170 client: AsyncClient,
171 db_session: AsyncSession,
172 ) -> None:
173 """The fork network JSON is injected into the page for the SVG DAG JS renderer.
174
175 window.__forkNetwork must be set in a <script> tag so the DAG renderer can
176 read it synchronously without an async fetch call.
177 """
178 source_id = await _make_repo(db_session)
179 await _make_fork(db_session, source_id, fork_owner="dave", fork_slug="bass-project")
180
181 response = await client.get("/upstream/bass-project/forks")
182 assert response.status_code == 200
183 body = response.text
184 assert "window.__forkNetwork" in body
185 # The fork owner must appear inside the injected JSON data block
186 assert "dave" in body