Commit 89601690f2

89601690f20e9384154e0b5e978448b7f60750be

parent: e9566eed86

Verified · cmc ci/build: success ci/test: success

cmc <hello@cleberg.net> · 2026-09-21 21:05 UTC

e2e: follow the profile's sections to their tabs

The activity graph, the About text and the organization's member and
team forms moved off /{owner}, so the tests that asserted on them there
were reading the repository list. Each now fetches the tab that holds
what it checks, and an outsider reaching the people tab gets the 404
the handler answers with rather than a page with the controls hidden.

Two assertions in TestOwnerProfiles described the old stack directly —
that the about must render above the activity graph, and that the
repository list must not. They are replaced by what the split
guarantees: the bare owner page is the repository list and carries
neither, and each of the other two is one tab along.

Ref #242
e2e/activity_test.go +4 −4
@@ -49,7 +49,7 @@ func TestActivityGraph(t *testing.T) {
4949 if _, _, code := inst.ssh(t, aliceKey, "", "issue", "create", "alice/app", "--title", "'x'"); code != 0 {
5050 t.Fatal("issue create failed")
5151 }
52 status, body := inst.get(t, "/alice")
52 status, body := inst.get(t, "/alice/-/activity")
5353 if status != 200 || !strings.Contains(body, `class="actgraph"`) {
5454 t.Fatalf("graph missing: %d", status)
5555 }
@@ -61,14 +61,14 @@ func TestActivityGraph(t *testing.T) {
6161 t.Fatalf("alice total = %d, want >= 2", total)
6262 }
6363 // bob authored a commit but his email is unverified: zero activity.
64 _, bobBody := inst.get(t, "/bob")
64 _, bobBody := inst.get(t, "/bob/-/activity")
6565 if bt := activityTotal(t, bobBody); bt != 0 {
6666 t.Fatalf("unverified author got credit: total %d", bt)
6767 }
6868
6969 // Re-pushing the same history (force) does not double-count.
7070 mustGit(t, dir, env, "push", "-q", "--force", "origin", "main")
71 _, body2 := inst.get(t, "/alice")
71 _, body2 := inst.get(t, "/alice/-/activity")
7272 if body2 != body {
7373 // Counts must be identical; compare just the graph cells.
7474 if excerpt(body, "actgraph") != excerpt(body2, "actgraph") {
@@ -83,7 +83,7 @@ func TestActivityGraph(t *testing.T) {
8383 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "transfer", "alice/app", "theorg"); code != 0 {
8484 t.Fatal("transfer failed")
8585 }
86 _, orgBody := inst.get(t, "/theorg")
86 _, orgBody := inst.get(t, "/theorg/-/activity")
8787 if !strings.Contains(orgBody, `class="actgraph"`) || strings.Contains(orgBody, "0 in the last year") {
8888 t.Fatalf("org graph empty:\n%s", excerpt(orgBody, "activity"))
8989 }
e2e/orgweb_test.go +9 −6
@@ -28,8 +28,11 @@ func TestOrgManagementWeb(t *testing.T) {
2828
2929 // The management sections are admin-only: bob is not even a member.
3030 bob := loginBrowser(t, inst, bobKey)
31 if _, body := browserGet(t, bob, inst.base()+"/acme"); strings.Contains(body, `value="member-add"`) {
32 t.Fatal("a non-member sees organization controls")
31 // The people tab is the admin panel, so an outsider gets the 404 a
32 // page nobody has rather than a page with the controls hidden.
33 if status, body := browserGet(t, bob, inst.base()+"/acme/-/people"); status != 404 ||
34 strings.Contains(body, `value="member-add"`) {
35 t.Fatalf("a non-member reaches the organization controls: %d", status)
3336 }
3437 // And POSTing anyway is refused by the command, not by the template.
3538 browserPost(t, bob, inst.base()+"/acme", url.Values{
@@ -39,7 +42,7 @@ func TestOrgManagementWeb(t *testing.T) {
3942 t.Fatalf("non-admin added themselves: %v", members)
4043 }
4144
42 status, body := browserGet(t, alice, inst.base()+"/acme")
45 status, body := browserGet(t, alice, inst.base()+"/acme/-/people")
4346 if status != 200 || !strings.Contains(body, `value="member-add"`) {
4447 t.Fatalf("admin sees no controls: %d", status)
4548 }
@@ -73,8 +76,8 @@ func TestOrgManagementWeb(t *testing.T) {
7376 t.Fatalf("team grant did not confer access: %s", errOut)
7477 }
7578
76 // The page shows what was built.
77 _, body = browserGet(t, alice, inst.base()+"/acme")
79 // The people tab shows what was built.
80 _, body = browserGet(t, alice, inst.base()+"/acme/-/people")
7881 for _, want := range []string{"builders", "acme/widget", "1 member"} {
7982 if !strings.Contains(body, want) {
8083 t.Errorf("org page missing %q", want)
@@ -180,7 +183,7 @@ func TestOrgLifecycleWeb(t *testing.T) {
180183 }
181184
182185 // Rename is offered to its admin, and the org moves.
183 _, body := browserGet(t, alice, inst.base()+"/acmeco")
186 _, body := browserGet(t, alice, inst.base()+"/acmeco/-/people")
184187 if !strings.Contains(body, `value="org-rename"`) {
185188 t.Fatalf("no rename form for the org admin:\n%s", body)
186189 }
e2e/profile_test.go +15 −11
@@ -157,7 +157,7 @@ func TestOwnerProfiles(t *testing.T) {
157157 }
158158 // Org emphasis parsed, not left as literal slashes the way the
159159 // markdown renderer would.
160 _, body = inst.get(t, "/alice")
160 _, body = inst.get(t, "/alice/-/about")
161161 if !strings.Contains(body, "<em>small tools</em>") || strings.Contains(body, "/small tools/") {
162162 t.Fatalf("org about not rendered as org: %s", body)
163163 }
@@ -196,25 +196,29 @@ func TestOwnerProfiles(t *testing.T) {
196196 t.Fatal("more than five links accepted")
197197 }
198198
199 // Owner pages render description and website link.
199 // The bare owner page is the repository list, with the description
200 // and the link chips in the header above the tabs (#242).
200201 status, body = inst.get(t, "/alice")
201202 if status != 200 || !strings.Contains(body, "tinkerer") {
202203 t.Fatalf("user page profile: %d", status)
203204 }
204 // About renders as markdown between the header and the activity graph;
205 // links render as chips.
206 if !strings.Contains(body, "<em>small tools</em>") {
207 t.Fatalf("about not rendered: %s", body)
208 }
209205 if !strings.Contains(body, `href="https://fosstodon.example/@alice"`) ||
210206 !strings.Contains(body, ">Mastodon<") {
211207 t.Fatalf("links not rendered: %s", body)
212208 }
213 if strings.Index(body, "<em>small tools</em>") > strings.Index(body, `class="activity"`) {
214 t.Error("about renders below the activity graph")
209 if !strings.Contains(body, `<ul class="repolist"`) {
210 t.Error("the owner page is not the repository list")
211 }
212 if strings.Contains(body, `class="activity"`) || strings.Contains(body, "<em>small tools</em>") {
213 t.Error("the about text or the activity graph still sits on the repository page")
214 }
215 // Each of them is one tab along, and the markdown renders there.
216 _, aboutBody := inst.get(t, "/alice/-/about")
217 if !strings.Contains(aboutBody, "<em>small tools</em>") {
218 t.Fatalf("about not rendered: %s", aboutBody)
215219 }
216 if strings.Index(body, `<ul class="repolist"`) < strings.Index(body, `class="activity"`) {
217 t.Error("repositories render above the activity graph")
220 if _, g := inst.get(t, "/alice/-/activity"); !strings.Contains(g, `class="activity"`) {
221 t.Error("the activity tab has no graph")
218222 }
219223
220224 // Clearing works the same way as the other fields. The about is not
e2e/profileabout_test.go +2 −2
@@ -39,7 +39,7 @@ func TestProfileAboutFromRepo(t *testing.T) {
3939 t.Errorf("about_path missing: %s", out)
4040 }
4141
42 _, body := inst.get(t, "/alice")
42 _, body := inst.get(t, "/alice/-/about")
4343 if !strings.Contains(body, "hello from a file") {
4444 t.Error("web profile does not render the about")
4545 }
@@ -53,7 +53,7 @@ func TestProfileAboutFromRepo(t *testing.T) {
5353 "--ref", "main", "--file", "-"); code != 0 {
5454 t.Fatalf("committing bob's org about: %s", errOut)
5555 }
56 if _, page := inst.get(t, "/bob"); !strings.Contains(page, "<em>note</em>") {
56 if _, page := inst.get(t, "/bob/-/about"); !strings.Contains(page, "<em>note</em>") {
5757 t.Errorf("about did not render as org:\n%s", page)
5858 }
5959}