Cover the issues/projects LiveViews; fix the unreachable project board

b03f932ac2bf · AtlantisPleb · · parent f8c8ad3e6630

Cover the issues/projects LiveViews; fix the unreachable project board

All eight LiveViews behind /:owner/:repo/{issues,labels,milestones,
projects,assignees} were at 0% line coverage. Testing them turned up a
real bug: ProjectShowLive read its kanban columns as `@statuses` inside
~H, where `@` resolves to `assigns.statuses` rather than the module
attribute of the same name. `mount/3` assigned `:status_options` but
never `:statuses`, so every render raised KeyError and the project board
route was unreachable in production. mount now assigns the list.

57 tests, one file per LiveView. Each covers mount, seeded records, the
empty state, and the interactions the view actually has: state filtering
and stream reset on the issue index; close/reopen, edit toggle, edit save
and comment posting on the issue show; create with labels and milestone
on the issue new form; create/delete on labels, milestones and projects;
close on milestones; add-to-board on the project show. Assertions are on
ids, aria-current, role and visible text, never on CSS classes, so the
basecoat migration does not rot them.

Also exercises ProjectItemsFixtures, which had no caller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149rBWy7br1Z7bbz9NrQhEr
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified lib/openagents_web/live/project_show_live.ex
  • added test/openagents_web/live/assignee_index_live_test.exs
  • added test/openagents_web/live/issue_index_live_test.exs
  • added test/openagents_web/live/issue_new_live_test.exs
  • added test/openagents_web/live/issue_show_live_test.exs
  • added test/openagents_web/live/label_index_live_test.exs
  • added test/openagents_web/live/milestone_index_live_test.exs
  • added test/openagents_web/live/project_index_live_test.exs
  • added test/openagents_web/live/project_show_live_test.exs

Diff

9 files changed, +985 -0

lib/openagents_web/live/project_show_live.ex modified +4

@@ -23,6 +23,10 @@ defmodule OpenAgentsWeb.ProjectShowLive do

23 23
     |> assign(:project, project)
24 24
     |> assign(:items, items)
25 25
     |> assign(:issue_options, issue_options)
26
     # The board columns are read as `@statuses` inside ~H, where `@` means
27
     # `assigns.statuses`, not the module attribute. Without this assign every
28
     # render raised KeyError and the route was unreachable.
29
     |> assign(:statuses, @statuses)
26 30
     |> assign(:status_options, Enum.map(@statuses, &{&1, &1}))
27 31
     |> assign(:form, to_form(ProjectItem.changeset(%ProjectItem{}, %{}), as: "item"))}
28 32
  end
test/openagents_web/live/assignee_index_live_test.exs added +47

@@ -0,0 +1,47 @@

1
defmodule OpenAgentsWeb.AssigneeIndexLiveTest do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  import Phoenix.LiveViewTest
5
6
  alias OpenAgents.Issues
7
8
  setup %{conn: conn} do
9
    {:ok, conn: log_in_github_user(conn, "assignee-index")}
10
  end
11
12
  test "mounts with an honest empty state when no issue has an assignee", %{conn: conn} do
13
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/sarah/assignees")
14
15
    assert html =~ "Assignees"
16
    assert has_element?(view, ~s{[role="status"]}, "No assignees have been assigned")
17
    refute has_element?(view, "#assignees")
18
  end
19
20
  test "tallies assignees across open and closed issues, most-assigned first", %{conn: conn} do
21
    {:ok, _first} = Issues.create_issue(%{"title" => "First", "assignees" => ["ada", "grace"]})
22
    {:ok, _second} = Issues.create_issue(%{"title" => "Second", "assignees" => ["ada"]})
23
    {:ok, third} = Issues.create_issue(%{"title" => "Third", "assignees" => ["ada"]})
24
25
    # A closed issue still counts: the view lists `state: "all"`.
26
    {:ok, _} = Issues.update_issue(third, %{"state" => "closed"})
27
28
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/sarah/assignees")
29
30
    assert has_element?(view, "#assignees")
31
    refute html =~ "No assignees have been assigned"
32
33
    # Rows are sorted by descending count, so `ada` (3) precedes `grace` (1).
34
    assert has_element?(view, "#assignees tr:first-child td:first-child", "ada")
35
    assert has_element?(view, "#assignees tr:first-child td:nth-child(2)", "3")
36
    assert has_element?(view, "#assignees tr:nth-child(2) td:first-child", "grace")
37
    assert has_element?(view, "#assignees tr:nth-child(2) td:nth-child(2)", "1")
38
    refute has_element?(view, "#assignees tr:nth-child(3)")
39
  end
40
41
  test "an anonymous visitor is redirected away from the assignee list" do
42
    assert {:error, {:redirect, %{to: to}}} =
43
             live(build_conn(), ~p"/OpenAgentsInc/sarah/assignees")
44
45
    refute to == "/OpenAgentsInc/sarah/assignees"
46
  end
47
end
test/openagents_web/live/issue_index_live_test.exs added +146

@@ -0,0 +1,146 @@

1
defmodule OpenAgentsWeb.IssueIndexLiveTest do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  import Phoenix.LiveViewTest
5
  import OpenAgents.LabelsFixtures
6
7
  alias OpenAgents.Issues
8
9
  setup %{conn: conn} do
10
    {:ok, conn: log_in_github_user(conn, "issue-index")}
11
  end
12
13
  test "mounts with zeroed counts and an empty state", %{conn: conn} do
14
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/sarah/issues")
15
16
    assert html =~ "No open issues"
17
    assert html =~ "Issues will show up here once they are created."
18
    refute has_element?(view, "#issues")
19
20
    # The default filter is `open`, and it is the one marked current.
21
    assert has_element?(view, ~s{a[href="/OpenAgentsInc/sarah/issues?state=open"][aria-current]})
22
23
    refute has_element?(
24
             view,
25
             ~s{a[href="/OpenAgentsInc/sarah/issues?state=closed"][aria-current]}
26
           )
27
28
    assert has_element?(view, ~s{a[href="/OpenAgentsInc/sarah/issues/new"]}, "New issue")
29
  end
30
31
  test "lists open issues with their number, author, labels, and assignees", %{conn: conn} do
32
    label_fixture(%{name: "bug", color: "d73a4a"})
33
34
    {:ok, issue} =
35
      Issues.create_issue(%{
36
        "title" => "Streaming stalls",
37
        "user" => %{"login" => "ada"},
38
        "labels" => ["bug"],
39
        "assignees" => ["grace"]
40
      })
41
42
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/sarah/issues")
43
44
    refute html =~ "No open issues"
45
    assert has_element?(view, "#issues")
46
47
    assert has_element?(
48
             view,
49
             ~s{a[href="/OpenAgentsInc/sarah/issues/#{issue.number}"]},
50
             "Streaming stalls"
51
           )
52
53
    assert html =~ "##{issue.number}"
54
    assert html =~ "ada"
55
    assert html =~ "bug"
56
    assert has_element?(view, ~s{[title="grace"]})
57
  end
58
59
  test "an issue with no author falls back to anonymous", %{conn: conn} do
60
    {:ok, _} = Issues.create_issue(%{"title" => "Orphaned"})
61
62
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/sarah/issues")
63
64
    assert html =~ "Orphaned"
65
    assert html =~ "anonymous"
66
  end
67
68
  test "the comment count only renders once an issue has comments", %{conn: conn} do
69
    {:ok, issue} = Issues.create_issue(%{"title" => "Chatty"})
70
71
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/sarah/issues")
72
    refute html =~ "hero-chat-bubble-left"
73
74
    {:ok, _} =
75
      Issues.create_comment(%{
76
        issue_id: issue.id,
77
        body: "First",
78
        user: %{"login" => "ada"}
79
      })
80
81
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/sarah/issues")
82
    assert html =~ "hero-chat-bubble-left"
83
  end
84
85
  test "patching to the closed filter swaps the stream and the current marker", %{conn: conn} do
86
    {:ok, _open} = Issues.create_issue(%{"title" => "Open one"})
87
    {:ok, closed} = Issues.create_issue(%{"title" => "Closed one"})
88
    {:ok, _} = Issues.update_issue(closed, %{"state" => "closed"})
89
90
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/sarah/issues")
91
    assert html =~ "Open one"
92
    refute html =~ "Closed one"
93
94
    html =
95
      view
96
      |> element(~s{a[href="/OpenAgentsInc/sarah/issues?state=closed"]})
97
      |> render_click()
98
99
    assert html =~ "Closed one"
100
    refute html =~ "Open one"
101
102
    assert has_element?(
103
             view,
104
             ~s{a[href="/OpenAgentsInc/sarah/issues?state=closed"][aria-current]}
105
           )
106
107
    refute has_element?(view, ~s{a[href="/OpenAgentsInc/sarah/issues?state=open"][aria-current]})
108
  end
109
110
  test "the closed filter has its own empty-state wording", %{conn: conn} do
111
    {:ok, _open} = Issues.create_issue(%{"title" => "Open one"})
112
113
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/sarah/issues?state=closed")
114
115
    assert html =~ "No closed issues"
116
    refute html =~ "No open issues"
117
  end
118
119
  test "the open and closed counts stay visible on both filters", %{conn: conn} do
120
    {:ok, _} = Issues.create_issue(%{"title" => "A"})
121
    {:ok, _} = Issues.create_issue(%{"title" => "B"})
122
    {:ok, c} = Issues.create_issue(%{"title" => "C"})
123
    {:ok, _} = Issues.update_issue(c, %{"state" => "closed"})
124
125
    for state <- ["open", "closed"] do
126
      {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/issues?state=#{state}")
127
128
      assert has_element?(
129
               view,
130
               ~s{a[href="/OpenAgentsInc/sarah/issues?state=open"]},
131
               "2 Open"
132
             )
133
134
      assert has_element?(
135
               view,
136
               ~s{a[href="/OpenAgentsInc/sarah/issues?state=closed"]},
137
               "1 Closed"
138
             )
139
    end
140
  end
141
142
  test "an anonymous visitor is redirected away from the issue list" do
143
    assert {:error, {:redirect, %{to: to}}} = live(build_conn(), ~p"/OpenAgentsInc/sarah/issues")
144
    refute to == "/OpenAgentsInc/sarah/issues"
145
  end
146
end
test/openagents_web/live/issue_new_live_test.exs added +114

@@ -0,0 +1,114 @@

1
defmodule OpenAgentsWeb.IssueNewLiveTest do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  import Phoenix.LiveViewTest
5
  import OpenAgents.LabelsFixtures
6
  import OpenAgents.MilestonesFixtures
7
8
  alias OpenAgents.Issues
9
10
  setup %{conn: conn} do
11
    {:ok, conn: log_in_github_user(conn, "issue-new")}
12
  end
13
14
  test "mounts with an empty form and a cancel link back to the list", %{conn: conn} do
15
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/sarah/issues/new")
16
17
    assert html =~ "New issue"
18
    assert has_element?(view, "#new-issue-form")
19
    assert has_element?(view, "#issue_title")
20
    assert has_element?(view, "#issue_body")
21
    assert has_element?(view, ~s{a[href="/OpenAgentsInc/sarah/issues"]}, "Cancel")
22
  end
23
24
  test "the milestone and label selects offer the seeded records", %{conn: conn} do
25
    milestone = milestone_fixture(%{title: "v1.0", due_on: nil})
26
    label_fixture(%{name: "bug", color: "d73a4a"})
27
28
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/issues/new")
29
30
    assert has_element?(view, ~s{#issue_milestone option[value="#{milestone.number}"]}, "v1.0")
31
    assert has_element?(view, ~s{#issue_labels option[value="bug"]}, "bug")
32
    assert has_element?(view, ~s{#issue_milestone option[value=""]}, "Select a milestone")
33
  end
34
35
  test "with no milestones or labels the selects render only the prompt", %{conn: conn} do
36
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/issues/new")
37
38
    assert has_element?(view, "#issue_milestone")
39
    refute has_element?(view, ~s{#issue_labels option})
40
  end
41
42
  test "submitting a title creates the issue and navigates to it", %{conn: conn} do
43
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/issues/new")
44
45
    result =
46
      view
47
      |> form("#new-issue-form", issue: %{title: "Add a runbook", body: "Please"})
48
      |> render_submit()
49
50
    assert [issue] = Issues.list_issues()
51
    assert issue.title == "Add a runbook"
52
    assert issue.body == "Please"
53
54
    {:ok, _show, html} =
55
      follow_redirect(result, conn, ~p"/OpenAgentsInc/sarah/issues/#{issue.number}")
56
57
    assert html =~ "Issue created"
58
    assert html =~ "Add a runbook"
59
  end
60
61
  test "a submitted label and milestone are applied to the new issue", %{conn: conn} do
62
    milestone = milestone_fixture(%{title: "v1.0", due_on: nil})
63
    label_fixture(%{name: "bug", color: "d73a4a"})
64
65
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/issues/new")
66
67
    assert {:error, {:live_redirect, _}} =
68
             view
69
             |> form("#new-issue-form",
70
               issue: %{
71
                 title: "Tagged",
72
                 body: "",
73
                 milestone: to_string(milestone.number),
74
                 labels: ["bug"]
75
               }
76
             )
77
             |> render_submit()
78
79
    assert [issue] = Issues.list_issues()
80
    assert [%{"name" => "bug", "color" => "d73a4a"}] = issue.labels
81
    assert issue.milestone["title"] == "v1.0"
82
    assert issue.milestone["number"] == milestone.number
83
  end
84
85
  test "an empty label and milestone selection leaves the issue bare", %{conn: conn} do
86
    label_fixture(%{name: "bug", color: "d73a4a"})
87
88
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/issues/new")
89
90
    assert {:error, {:live_redirect, _}} =
91
             view
92
             |> form("#new-issue-form",
93
               issue: %{title: "Bare", body: "", milestone: "", labels: []}
94
             )
95
             |> render_submit()
96
97
    assert [issue] = Issues.list_issues()
98
    assert issue.labels == []
99
    assert issue.milestone == nil
100
  end
101
102
  test "a blank title re-renders the form with an error and creates nothing", %{conn: conn} do
103
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/issues/new")
104
105
    html =
106
      view
107
      |> form("#new-issue-form", issue: %{title: "", body: "no title"})
108
      |> render_submit()
109
110
    assert html =~ "can&#39;t be blank"
111
    assert has_element?(view, "#new-issue-form")
112
    assert Issues.list_issues(state: "all") == []
113
  end
114
end
test/openagents_web/live/issue_show_live_test.exs added +211

@@ -0,0 +1,211 @@

1
defmodule OpenAgentsWeb.IssueShowLiveTest do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  import Phoenix.LiveViewTest
5
  import OpenAgents.LabelsFixtures
6
  import OpenAgents.MilestonesFixtures
7
8
  alias OpenAgents.Issues
9
10
  setup %{conn: conn} do
11
    {:ok, conn: log_in_github_user(conn, "issue-show")}
12
  end
13
14
  defp issue!(attrs) do
15
    {:ok, issue} = Issues.create_issue(attrs)
16
    issue
17
  end
18
19
  defp path(issue), do: ~p"/OpenAgentsInc/sarah/issues/#{issue.number}"
20
21
  test "mounts and renders the title, number, state, and body", %{conn: conn} do
22
    issue =
23
      issue!(%{
24
        "title" => "Streaming stalls",
25
        "body" => "It hangs",
26
        "user" => %{"login" => "ada"}
27
      })
28
29
    {:ok, view, html} = live(conn, path(issue))
30
31
    assert html =~ "Streaming stalls"
32
    assert html =~ "##{issue.number}"
33
    assert html =~ "It hangs"
34
    assert html =~ "ada"
35
    assert has_element?(view, ~s{button[phx-click="close"]}, "Close issue")
36
    refute has_element?(view, ~s{button[phx-click="reopen"]})
37
  end
38
39
  test "an issue with no body says so rather than rendering blank", %{conn: conn} do
40
    issue = issue!(%{"title" => "Bare"})
41
42
    {:ok, _view, html} = live(conn, path(issue))
43
44
    assert html =~ "No description provided."
45
    assert html =~ "anonymous"
46
  end
47
48
  test "the sidebar only renders sections the issue actually has", %{conn: conn} do
49
    bare = issue!(%{"title" => "Bare"})
50
    {:ok, _view, html} = live(conn, path(bare))
51
52
    refute html =~ "Labels"
53
    refute html =~ "Assignees"
54
    refute html =~ "Milestone"
55
56
    label_fixture(%{name: "bug", color: "d73a4a"})
57
    milestone = milestone_fixture(%{title: "v1.0", due_on: nil})
58
59
    rich = issue!(%{"title" => "Rich", "labels" => ["bug"], "assignees" => ["grace"]})
60
    {:ok, rich} = Issues.set_milestone(rich, milestone.number)
61
62
    {:ok, view, html} = live(conn, path(rich))
63
64
    assert html =~ "Labels"
65
    assert html =~ "bug"
66
    assert html =~ "Assignees"
67
    assert has_element?(view, ~s{[title="grace"]})
68
    assert has_element?(view, ~s{a[href="/OpenAgentsInc/sarah/milestones"]}, "v1.0")
69
  end
70
71
  test "a missing issue number raises rather than rendering an empty page", %{conn: conn} do
72
    assert_raise Ecto.NoResultsError, fn ->
73
      live(conn, ~p"/OpenAgentsInc/sarah/issues/9999")
74
    end
75
  end
76
77
  test "closing then reopening an issue swaps the action buttons", %{conn: conn} do
78
    issue = issue!(%{"title" => "Toggle me"})
79
    {:ok, view, _html} = live(conn, path(issue))
80
81
    html = view |> element(~s{button[phx-click="close"]}) |> render_click()
82
83
    assert html =~ "Issue closed"
84
    assert has_element?(view, ~s{button[phx-click="reopen"]}, "Reopen issue")
85
    refute has_element?(view, ~s{button[phx-click="close"]})
86
87
    closed = Issues.get_issue!(issue.id)
88
    assert closed.state == "closed"
89
    assert closed.state_reason == "completed"
90
    assert closed.closed_at
91
92
    html = view |> element(~s{button[phx-click="reopen"]}) |> render_click()
93
94
    assert html =~ "Issue reopened"
95
    assert has_element?(view, ~s{button[phx-click="close"]}, "Close issue")
96
97
    reopened = Issues.get_issue!(issue.id)
98
    assert reopened.state == "open"
99
    assert reopened.closed_at == nil
100
  end
101
102
  test "the edit toggle swaps the header for the edit form and back", %{conn: conn} do
103
    issue = issue!(%{"title" => "Editable", "body" => "Before"})
104
    {:ok, view, _html} = live(conn, path(issue))
105
106
    refute has_element?(view, "#issue-edit-form")
107
108
    view |> element(~s{button[phx-click="toggle_edit"]}) |> render_click()
109
    assert has_element?(view, "#issue-edit-form")
110
    refute has_element?(view, ~s{button[phx-click="close"]})
111
112
    view |> element(~s{#issue-edit-form button[phx-click="toggle_edit"]}) |> render_click()
113
    refute has_element?(view, "#issue-edit-form")
114
    assert has_element?(view, ~s{button[phx-click="close"]})
115
  end
116
117
  test "saving the edit form updates the issue and leaves edit mode", %{conn: conn} do
118
    issue = issue!(%{"title" => "Editable", "body" => "Before"})
119
    {:ok, view, _html} = live(conn, path(issue))
120
121
    view |> element(~s{button[phx-click="toggle_edit"]}) |> render_click()
122
123
    html =
124
      view
125
      |> form("#issue-edit-form", issue: %{title: "Edited", body: "After"})
126
      |> render_submit()
127
128
    assert html =~ "Issue updated"
129
    assert html =~ "Edited"
130
    assert html =~ "After"
131
    refute has_element?(view, "#issue-edit-form")
132
133
    updated = Issues.get_issue!(issue.id)
134
    assert updated.title == "Edited"
135
    assert updated.body == "After"
136
  end
137
138
  test "clearing the title in the edit form keeps the form and shows the error", %{conn: conn} do
139
    issue = issue!(%{"title" => "Editable"})
140
    {:ok, view, _html} = live(conn, path(issue))
141
142
    view |> element(~s{button[phx-click="toggle_edit"]}) |> render_click()
143
144
    html =
145
      view
146
      |> form("#issue-edit-form", issue: %{title: "", body: "still here"})
147
      |> render_submit()
148
149
    assert html =~ "can&#39;t be blank"
150
    assert has_element?(view, "#issue-edit-form")
151
    assert Issues.get_issue!(issue.id).title == "Editable"
152
  end
153
154
  test "adding a comment appends it to the thread and bumps the count", %{conn: conn} do
155
    issue = issue!(%{"title" => "Discuss"})
156
    {:ok, view, html} = live(conn, path(issue))
157
158
    assert has_element?(view, "#comment-form")
159
    refute html =~ "Looks good to me"
160
161
    html =
162
      view
163
      |> form("#comment-form", comment: %{body: "Looks good to me"})
164
      |> render_submit()
165
166
    assert html =~ "Comment added"
167
    assert html =~ "Looks good to me"
168
    assert html =~ "anonymous"
169
170
    assert [comment] = Issues.list_comments(issue.id)
171
    assert comment.body == "Looks good to me"
172
    assert Issues.get_issue!(issue.id).comments == 1
173
  end
174
175
  test "an empty comment body is rejected and nothing is stored", %{conn: conn} do
176
    issue = issue!(%{"title" => "Discuss"})
177
    {:ok, view, _html} = live(conn, path(issue))
178
179
    html =
180
      view
181
      |> form("#comment-form", comment: %{body: ""})
182
      |> render_submit()
183
184
    assert html =~ "can&#39;t be blank"
185
    assert Issues.list_comments(issue.id) == []
186
    assert Issues.get_issue!(issue.id).comments == 0
187
  end
188
189
  test "existing comments render with their author on mount", %{conn: conn} do
190
    issue = issue!(%{"title" => "Discuss"})
191
192
    {:ok, _} =
193
      Issues.create_comment(%{
194
        issue_id: issue.id,
195
        body: "Earlier note",
196
        user: %{"login" => "ada"}
197
      })
198
199
    {:ok, _view, html} = live(conn, path(issue))
200
201
    assert html =~ "Earlier note"
202
    assert html =~ "ada"
203
  end
204
205
  test "an anonymous visitor is redirected away from an issue page" do
206
    issue = issue!(%{"title" => "Private"})
207
208
    assert {:error, {:redirect, %{to: to}}} = live(build_conn(), path(issue))
209
    refute to == "/OpenAgentsInc/sarah/issues/#{issue.number}"
210
  end
211
end
test/openagents_web/live/label_index_live_test.exs added +78

@@ -0,0 +1,78 @@

1
defmodule OpenAgentsWeb.LabelIndexLiveTest do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  import Phoenix.LiveViewTest
5
  import OpenAgents.LabelsFixtures
6
7
  alias OpenAgents.Labels
8
9
  setup %{conn: conn} do
10
    {:ok, conn: log_in_github_user(conn, "label-index")}
11
  end
12
13
  test "mounts with the create form and an empty state", %{conn: conn} do
14
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/sarah/labels")
15
16
    assert html =~ "Labels"
17
    assert has_element?(view, "#new-label-form")
18
    assert has_element?(view, ~s{[role="status"]}, "No labels yet")
19
    refute has_element?(view, "#labels")
20
  end
21
22
  test "lists seeded labels with their descriptions", %{conn: conn} do
23
    label_fixture(%{name: "bug", color: "d73a4a", description: "Something is broken"})
24
    label_fixture(%{name: "docs", color: "0075ca", description: nil})
25
26
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/sarah/labels")
27
28
    refute html =~ "No labels yet"
29
    assert has_element?(view, "#labels")
30
    assert has_element?(view, "#labels td", "bug")
31
    assert has_element?(view, "#labels td", "Something is broken")
32
    assert has_element?(view, "#labels td", "docs")
33
    # A label with no description renders the em-dash placeholder, not "".
34
    assert has_element?(view, "#labels td", "—")
35
  end
36
37
  test "submitting the form creates a label and re-renders the table", %{conn: conn} do
38
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/labels")
39
40
    html =
41
      view
42
      |> form("#new-label-form", label: %{name: "enhancement", color: "a2eeef"})
43
      |> render_submit()
44
45
    assert html =~ "Label created"
46
    assert has_element?(view, "#labels td", "enhancement")
47
    assert [%Labels.Label{name: "enhancement"}] = Labels.list_labels()
48
  end
49
50
  test "a label missing its required color re-renders the form with an error", %{conn: conn} do
51
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/labels")
52
53
    html =
54
      view
55
      |> form("#new-label-form", label: %{name: "no-color", color: ""})
56
      |> render_submit()
57
58
    assert html =~ "can&#39;t be blank"
59
    assert has_element?(view, "#new-label-form")
60
    assert Labels.list_labels() == []
61
  end
62
63
  test "deleting a label removes it and returns the empty state", %{conn: conn} do
64
    label = label_fixture(%{name: "wontfix", color: "ffffff"})
65
66
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/labels")
67
    assert has_element?(view, "#labels td", "wontfix")
68
69
    html =
70
      view
71
      |> element(~s{#labels button[phx-value-id="#{label.id}"]})
72
      |> render_click()
73
74
    assert html =~ "Label deleted"
75
    assert has_element?(view, ~s{[role="status"]}, "No labels yet")
76
    assert Labels.list_labels() == []
77
  end
78
end
test/openagents_web/live/milestone_index_live_test.exs added +124

@@ -0,0 +1,124 @@

1
defmodule OpenAgentsWeb.MilestoneIndexLiveTest do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  import Phoenix.LiveViewTest
5
  import OpenAgents.MilestonesFixtures
6
7
  alias OpenAgents.Issues
8
  alias OpenAgents.Milestones
9
10
  setup %{conn: conn} do
11
    {:ok, conn: log_in_github_user(conn, "milestone-index")}
12
  end
13
14
  test "mounts with the create form and an empty state", %{conn: conn} do
15
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/sarah/milestones")
16
17
    assert html =~ "Milestones"
18
    assert has_element?(view, "#new-milestone-form")
19
    assert has_element?(view, ~s{[role="status"]}, "No milestones yet")
20
  end
21
22
  test "lists seeded milestones with their state and due date", %{conn: conn} do
23
    milestone_fixture(%{title: "v1.0", state: "open", due_on: "2026-12-31", description: "Ship"})
24
25
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/sarah/milestones")
26
27
    refute html =~ "No milestones yet"
28
    assert html =~ "v1.0"
29
    assert html =~ "Ship"
30
    assert html =~ "Due 2026-12-31"
31
  end
32
33
  test "the progress meter reports the closed-to-total ratio", %{conn: conn} do
34
    milestone = milestone_fixture(%{title: "Beta", state: "open", due_on: nil})
35
36
    {:ok, open_issue} = Issues.create_issue(%{"title" => "Still open"})
37
    {:ok, _} = Issues.set_milestone(open_issue, milestone.number)
38
39
    {:ok, done_issue} = Issues.create_issue(%{"title" => "Finished"})
40
    {:ok, done_issue} = Issues.set_milestone(done_issue, milestone.number)
41
    {:ok, _} = Issues.update_issue(done_issue, %{"state" => "closed"})
42
43
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/sarah/milestones")
44
45
    assert has_element?(
46
             view,
47
             ~s{[role="progressbar"][aria-label="Beta progress"][aria-valuenow="50"]}
48
           )
49
50
    assert html =~ "1 open"
51
    assert html =~ "1 closed"
52
    assert html =~ "2 total"
53
    assert html =~ "50%"
54
  end
55
56
  test "a milestone with no issues reports zero progress", %{conn: conn} do
57
    milestone_fixture(%{title: "Empty", due_on: nil})
58
59
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/milestones")
60
61
    assert has_element?(
62
             view,
63
             ~s{[role="progressbar"][aria-label="Empty progress"][aria-valuenow="0"]}
64
           )
65
  end
66
67
  test "submitting the form creates a milestone", %{conn: conn} do
68
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/milestones")
69
70
    html =
71
      view
72
      |> form("#new-milestone-form",
73
        milestone: %{title: "v2.0", due_on: "2027-01-01", description: "Next"}
74
      )
75
      |> render_submit()
76
77
    assert html =~ "Milestone created"
78
    assert html =~ "v2.0"
79
    assert [%Milestones.Milestone{title: "v2.0", number: 1}] = Milestones.list_milestones()
80
  end
81
82
  test "a milestone with no title re-renders the form with an error", %{conn: conn} do
83
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/milestones")
84
85
    html =
86
      view
87
      |> form("#new-milestone-form", milestone: %{title: "", due_on: "", description: ""})
88
      |> render_submit()
89
90
    assert html =~ "can&#39;t be blank"
91
    assert Milestones.list_milestones() == []
92
  end
93
94
  test "closing a milestone flips its state and hides the close button", %{conn: conn} do
95
    milestone = milestone_fixture(%{title: "Closeable", state: "open", due_on: nil})
96
97
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/milestones")
98
    assert has_element?(view, ~s{button[phx-click="close"][phx-value-id="#{milestone.id}"]})
99
100
    html =
101
      view
102
      |> element(~s{button[phx-click="close"][phx-value-id="#{milestone.id}"]})
103
      |> render_click()
104
105
    assert html =~ "Milestone closed"
106
    refute has_element?(view, ~s{button[phx-click="close"][phx-value-id="#{milestone.id}"]})
107
    assert Milestones.get_milestone!(milestone.id).state == "closed"
108
  end
109
110
  test "deleting a milestone returns the empty state", %{conn: conn} do
111
    milestone = milestone_fixture(%{title: "Deletable", due_on: nil})
112
113
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/milestones")
114
115
    html =
116
      view
117
      |> element(~s{button[phx-click="delete"][phx-value-id="#{milestone.id}"]})
118
      |> render_click()
119
120
    assert html =~ "Milestone deleted"
121
    assert has_element?(view, ~s{[role="status"]}, "No milestones yet")
122
    assert Milestones.list_milestones() == []
123
  end
124
end
test/openagents_web/live/project_index_live_test.exs added +95

@@ -0,0 +1,95 @@

1
defmodule OpenAgentsWeb.ProjectIndexLiveTest do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  import Phoenix.LiveViewTest
5
  import OpenAgents.ProjectsFixtures
6
7
  alias OpenAgents.Projects
8
9
  setup %{conn: conn} do
10
    {:ok, conn: log_in_github_user(conn, "project-index")}
11
  end
12
13
  test "mounts with the create form and an empty state", %{conn: conn} do
14
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/sarah/projects")
15
16
    assert html =~ "Projects"
17
    assert has_element?(view, "#new-project-form")
18
    assert has_element?(view, ~s{[role="status"]}, "No projects yet")
19
  end
20
21
  test "lists projects owned by the URL owner and links to each board", %{conn: conn} do
22
    project = project_fixture(%{title: "Roadmap", owner: "OpenAgentsInc", state: "open"})
23
24
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/sarah/projects")
25
26
    refute html =~ "No projects yet"
27
    assert html =~ "Roadmap"
28
29
    assert has_element?(
30
             view,
31
             ~s{a[href="/OpenAgentsInc/sarah/projects/#{project.number}"]},
32
             "Roadmap"
33
           )
34
35
    assert has_element?(
36
             view,
37
             ~s{a[href="/OpenAgentsInc/sarah/projects/#{project.number}"]},
38
             "View"
39
           )
40
  end
41
42
  test "a project owned by someone else is filtered out", %{conn: conn} do
43
    project_fixture(%{title: "Someone elses", owner: "other-org"})
44
45
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/sarah/projects")
46
47
    refute html =~ "Someone elses"
48
    assert html =~ "No projects yet"
49
  end
50
51
  test "submitting the form creates a project owned by the URL owner", %{conn: conn} do
52
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/projects")
53
54
    html =
55
      view
56
      |> form("#new-project-form", project: %{title: "Q3 delivery"})
57
      |> render_submit()
58
59
    assert html =~ "Project created"
60
    assert html =~ "Q3 delivery"
61
62
    assert [project] = Projects.list_projects()
63
    assert project.title == "Q3 delivery"
64
    assert project.owner == "OpenAgentsInc"
65
    assert project.state == "open"
66
  end
67
68
  test "a project with no title re-renders the form with an error", %{conn: conn} do
69
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/projects")
70
71
    html =
72
      view
73
      |> form("#new-project-form", project: %{title: ""})
74
      |> render_submit()
75
76
    assert html =~ "can&#39;t be blank"
77
    assert has_element?(view, "#new-project-form")
78
    assert Projects.list_projects() == []
79
  end
80
81
  test "deleting a project returns the empty state", %{conn: conn} do
82
    project = project_fixture(%{title: "Doomed", owner: "OpenAgentsInc"})
83
84
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/sarah/projects")
85
86
    html =
87
      view
88
      |> element(~s{button[phx-click="delete"][phx-value-id="#{project.id}"]})
89
      |> render_click()
90
91
    assert html =~ "Project deleted"
92
    assert has_element?(view, ~s{[role="status"]}, "No projects yet")
93
    assert Projects.list_projects() == []
94
  end
95
end
test/openagents_web/live/project_show_live_test.exs added +166

@@ -0,0 +1,166 @@

1
defmodule OpenAgentsWeb.ProjectShowLiveTest do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  import Phoenix.LiveViewTest
5
  import OpenAgents.LabelsFixtures
6
  import OpenAgents.ProjectItemsFixtures
7
  import OpenAgents.ProjectsFixtures
8
9
  alias OpenAgents.Issues
10
  alias OpenAgents.Projects
11
12
  setup %{conn: conn} do
13
    {:ok, conn: log_in_github_user(conn, "project-show")}
14
  end
15
16
  defp project!, do: project_fixture(%{title: "Roadmap", owner: "OpenAgentsInc"})
17
18
  defp path(project), do: ~p"/OpenAgentsInc/sarah/projects/#{project.number}"
19
20
  test "mounts and renders every board column plus the add form", %{conn: conn} do
21
    project = project!()
22
23
    {:ok, view, html} = live(conn, path(project))
24
25
    assert html =~ "Roadmap"
26
    assert html =~ "To Do"
27
    assert html =~ "In Progress"
28
    assert html =~ "Done"
29
    assert has_element?(view, "#new-project-item-form")
30
    assert has_element?(view, ~s{a[href="/OpenAgentsInc/sarah/projects"]}, "Back to projects")
31
  end
32
33
  test "an empty board renders the columns with no cards", %{conn: conn} do
34
    project = project!()
35
36
    {:ok, view, _html} = live(conn, path(project))
37
38
    refute has_element?(view, ~s{a[href^="/OpenAgentsInc/sarah/issues/"]})
39
    # With no issues in the repo the issue select carries only its prompt.
40
    refute has_element?(view, ~s{#item_issue_number option:not([value=""])})
41
  end
42
43
  test "the issue select offers every issue, open or closed", %{conn: conn} do
44
    project = project!()
45
    {:ok, open} = Issues.create_issue(%{"title" => "Still open"})
46
    {:ok, closed} = Issues.create_issue(%{"title" => "All done"})
47
    {:ok, _} = Issues.update_issue(closed, %{"state" => "closed"})
48
49
    {:ok, view, _html} = live(conn, path(project))
50
51
    assert has_element?(
52
             view,
53
             ~s{#item_issue_number option[value="#{open.number}"]},
54
             "##{open.number} Still open"
55
           )
56
57
    assert has_element?(
58
             view,
59
             ~s{#item_issue_number option[value="#{closed.number}"]},
60
             "##{closed.number} All done"
61
           )
62
63
    for status <- ["To Do", "In Progress", "Done"] do
64
      assert has_element?(view, ~s{#item_status option[value="#{status}"]}, status)
65
    end
66
  end
67
68
  test "an existing item renders as a card in its status column", %{conn: conn} do
69
    project = project!()
70
    label_fixture(%{name: "bug", color: "d73a4a"})
71
    {:ok, issue} = Issues.create_issue(%{"title" => "Fix the parser", "labels" => ["bug"]})
72
73
    {:ok, _item} =
74
      Projects.create_project_item(
75
        %{"issue_number" => issue.number, "values" => %{"Status" => "In Progress"}},
76
        project.id
77
      )
78
79
    {:ok, view, html} = live(conn, path(project))
80
81
    assert html =~ "Fix the parser"
82
    assert html =~ "bug"
83
84
    assert has_element?(
85
             view,
86
             ~s{a[href="/OpenAgentsInc/sarah/issues/#{issue.number}"]},
87
             "Fix the parser"
88
           )
89
  end
90
91
  test "an item with no recorded status falls back to To Do", %{conn: conn} do
92
    project = project!()
93
    {:ok, issue} = Issues.create_issue(%{"title" => "Unsorted"})
94
95
    {:ok, _item} =
96
      Projects.create_project_item(
97
        %{"issue_number" => issue.number, "values" => %{}},
98
        project.id
99
      )
100
101
    {:ok, _view, html} = live(conn, path(project))
102
103
    assert html =~ "Unsorted"
104
  end
105
106
  test "submitting the form adds the issue to the board", %{conn: conn} do
107
    project = project!()
108
    {:ok, issue} = Issues.create_issue(%{"title" => "Ship the runbook"})
109
110
    {:ok, view, html} = live(conn, path(project))
111
    refute html =~ "Ship the runbook</a>"
112
113
    html =
114
      view
115
      |> form("#new-project-item-form",
116
        item: %{issue_number: to_string(issue.number), status: "Done"}
117
      )
118
      |> render_submit()
119
120
    assert html =~ "Issue added to project"
121
122
    assert has_element?(
123
             view,
124
             ~s{a[href="/OpenAgentsInc/sarah/issues/#{issue.number}"]},
125
             "Ship the runbook"
126
           )
127
128
    assert [item] = Projects.list_project_items(project.id)
129
    assert item.issue_id == issue.id
130
    assert item.values == %{"Status" => "Done"}
131
  end
132
133
  test "a fixture-built item lands in the column named by its Status value", %{conn: conn} do
134
    project = project!()
135
    {:ok, issue} = Issues.create_issue(%{"title" => "Fixture-placed"})
136
137
    item =
138
      project_item_fixture(%{
139
        project_id: project.id,
140
        issue_id: issue.id,
141
        values: %{"Status" => "Done"}
142
      })
143
144
    assert item.project_id == project.id
145
146
    {:ok, view, _html} = live(conn, path(project))
147
148
    # Cards are grouped by column; the "Done" column is the third section.
149
    assert has_element?(
150
             view,
151
             ~s{section:nth-of-type(3) a[href="/OpenAgentsInc/sarah/issues/#{issue.number}"]},
152
             "Fixture-placed"
153
           )
154
155
    refute has_element?(
156
             view,
157
             ~s{section:nth-of-type(1) a[href="/OpenAgentsInc/sarah/issues/#{issue.number}"]}
158
           )
159
  end
160
161
  test "a missing project number raises rather than rendering an empty board", %{conn: conn} do
162
    assert_raise Ecto.NoResultsError, fn ->
163
      live(conn, ~p"/OpenAgentsInc/sarah/projects/9999")
164
    end
165
  end
166
end

This page updates live while a promote is in flight · changelog