defmodule OpenAgentsWeb.IssueIndexLiveTest do
use OpenAgentsWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import OpenAgents.AccountsFixtures
import OpenAgents.LabelsFixtures
alias OpenAgents.Issues
setup %{conn: conn} do
{:ok, conn: log_in_repository_user(conn, "issue-index", repository())}
end
test "mounts with zeroed counts and an empty state", %{conn: conn} do
{:ok, view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
assert html =~ "No open issues"
assert html =~ "Issues will show up here once they are created."
refute has_element?(view, "#issues")
# The default filter is `open`, and it is the one marked current.
assert has_element?(
view,
~s{a[href="/OpenAgentsInc/openagents.com/issues?state=open"][aria-current]}
)
refute has_element?(
view,
~s{a[href="/OpenAgentsInc/openagents.com/issues?state=closed"][aria-current]}
)
assert has_element?(view, ~s{a[href="/OpenAgentsInc/openagents.com/issues/new"]}, "New issue")
end
test "lists open issues with their number, author, labels, and assignees", %{conn: conn} do
grace = repository_user_fixture("grace-index")
{:ok, _membership} = OpenAgents.Repositories.add_member(repository(), grace, "contributor")
label_fixture(repository(), %{name: "bug", color: "d73a4a"})
{:ok, issue} =
Issues.create_issue(repository(), %{
"title" => "Streaming stalls",
"user" => %{"login" => "ada"},
"labels" => ["bug"],
"assignees" => ["grace-index"]
})
{:ok, view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
refute html =~ "No open issues"
assert has_element?(view, "#issues")
assert has_element?(
view,
~s{a[href="/OpenAgentsInc/openagents.com/issues/#{issue.number}"]},
"Streaming stalls"
)
assert html =~ "##{issue.number}"
assert html =~ "ada"
assert html =~ "bug"
assert has_element?(view, ~s{[title="grace-index"]})
end
test "an issue with no author falls back to anonymous", %{conn: conn} do
{:ok, _} = Issues.create_issue(repository(), %{"title" => "Orphaned"})
{:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
assert html =~ "Orphaned"
assert html =~ "anonymous"
end
test "the comment count only renders once an issue has comments", %{conn: conn} do
{:ok, issue} = Issues.create_issue(repository(), %{"title" => "Chatty"})
{:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
refute has_element?(view, ~s{svg[data-icon="comment"]})
{:ok, _} =
Issues.create_comment(%{
issue_id: issue.id,
body: "First",
user: %{"login" => "ada"}
})
{:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
assert has_element?(view, ~s{svg[data-icon="comment"]})
end
test "patching to the closed filter swaps the stream and the current marker", %{conn: conn} do
{:ok, _open} = Issues.create_issue(repository(), %{"title" => "Open one"})
{:ok, closed} = Issues.create_issue(repository(), %{"title" => "Closed one"})
{:ok, _} = Issues.update_issue(closed, %{"state" => "closed"})
{:ok, view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
assert html =~ "Open one"
refute html =~ "Closed one"
html =
view
|> element(~s{a[href="/OpenAgentsInc/openagents.com/issues?state=closed"]})
|> render_click()
assert html =~ "Closed one"
refute html =~ "Open one"
assert has_element?(
view,
~s{a[href="/OpenAgentsInc/openagents.com/issues?state=closed"][aria-current]}
)
refute has_element?(
view,
~s{a[href="/OpenAgentsInc/openagents.com/issues?state=open"][aria-current]}
)
end
test "the closed filter has its own empty-state wording", %{conn: conn} do
{:ok, _open} = Issues.create_issue(repository(), %{"title" => "Open one"})
{:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues?state=closed")
assert html =~ "No closed issues"
refute html =~ "No open issues"
end
test "the open and closed counts stay visible on both filters", %{conn: conn} do
{:ok, _} = Issues.create_issue(repository(), %{"title" => "A"})
{:ok, _} = Issues.create_issue(repository(), %{"title" => "B"})
{:ok, c} = Issues.create_issue(repository(), %{"title" => "C"})
{:ok, _} = Issues.update_issue(c, %{"state" => "closed"})
for state <- ["open", "closed"] do
{:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues?state=#{state}")
assert has_element?(
view,
~s{a[href="/OpenAgentsInc/openagents.com/issues?state=open"]},
"2 Open"
)
assert has_element?(
view,
~s{a[href="/OpenAgentsInc/openagents.com/issues?state=closed"]},
"1 Closed"
)
end
end
# In Circle the row's parts are selectors. Only two of them survive here:
# state and assignee are the facts worth changing without opening the issue,
# and both are GitHub fields. Labels and milestone need option lists longer
# than a row can explain, so they are edited from the issue page's rail.
test "closing an issue from its row drops it out of the open filter", %{conn: conn} do
{:ok, issue} = Issues.create_issue(repository(), %{"title" => "Closeable"})
{:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
assert has_element?(view, ~s{#row-state-#{issue.id}})
view
|> element(~s{#row-state-#{issue.id} button}, "Closed as not planned")
|> render_click()
closed = Issues.get_issue!(repository(), issue.id)
assert closed.state == "closed"
assert closed.state_reason == "not_planned"
# The list is filtered to open issues, so a closed one has to leave it --
# a row that stays visible after being closed is worse than a reload.
refute has_element?(view, ~s{#row-state-#{issue.id}})
assert has_element?(
view,
~s{a[href="/OpenAgentsInc/openagents.com/issues?state=closed"]},
"1 Closed"
)
end
test "assigning from a row keeps the row and updates the face", %{conn: conn} do
hopper = repository_user_fixture("hopper-index")
{:ok, _membership} = OpenAgents.Repositories.add_member(repository(), hopper, "contributor")
{:ok, issue} = Issues.create_issue(repository(), %{"title" => "Assignable"})
{:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
view
|> element(~s{#row-assignee-#{issue.id} button}, "hopper-index")
|> render_click()
assert Issues.get_issue!(repository(), issue.id).assignees |> Enum.map(& &1["login"]) == [
"hopper-index"
]
assert has_element?(view, ~s{#row-assignee-#{issue.id}})
assert has_element?(view, ~s{[title="hopper-index"]})
end
test "the row's controls sit outside the link to the issue", %{conn: conn} do
# A state-changing control inside a link target is how people mis-click.
{:ok, issue} = Issues.create_issue(repository(), %{"title" => "Separate hit areas"})
{:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
title_link =
view
|> render()
|> LazyHTML.from_fragment()
|> LazyHTML.query("a.issue-row__title")
|> LazyHTML.to_tree()
assert title_link != []
refute inspect(title_link) =~ "row-state-#{issue.id}"
refute inspect(title_link) =~ "row-assignee-#{issue.id}"
end
# Reading a public repository's issues is a public activity now, the same
# way reading its code is: no redirect, no controls, and an invitation to
# sign in rather than a wall.
test "an anonymous visitor reads a public repository's issue list" do
{:ok, _issue} = Issues.create_issue(repository(), %{"title" => "Public spectable"})
{:ok, view, html} = live(build_conn(), ~p"/OpenAgentsInc/openagents.com/issues")
assert html =~ "Public spectable"
refute has_element?(view, "#issues-empty")
# No triage controls, no filing link: the toolbar actions need authority.
refute has_element?(view, ~s{a[href="/OpenAgentsInc/openagents.com/issues/new"]})
refute has_element?(view, ~s{[id^="row-state-"]})
refute has_element?(view, ~s{[id^="row-assignee-"]})
end
test "an anonymous visitor cannot open a private repository's issues" do
assert_raise OpenAgentsWeb.PublicNotFoundError, fn ->
live(build_conn(), ~p"/SecondOrg/hidden-repo/issues")
end
end
# The arc has shipped in the component set since Circle landed and had no
# data behind it. It reads the same derived value the API publishes as
# `issue.openagents.progress`.
test "an issue a board has started renders the started arc", %{conn: conn} do
{:ok, started} = Issues.create_issue(repository(), %{"title" => "Underway"})
{:ok, queued} = Issues.create_issue(repository(), %{"title" => "Queued"})
{:ok, project} =
OpenAgents.Projects.create_project(repository(), %{title: "Board", owner: "OpenAgents"})
{:ok, _item} =
OpenAgents.ProjectItems.create_project_item(repository(), %{
project_id: project.id,
issue_id: started.id,
issue_repository_id: started.repository_id,
values: %{"Status" => "In Progress"}
})
{:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
assert has_element?(
view,
~s{#issues-#{started.id} .issue-status[data-category="started"] .issue-status__arc}
)
assert has_element?(view, ~s{[aria-label="In progress"]})
refute has_element?(view, ~s{#issues-#{queued.id} .issue-status[data-category="started"]})
end
# The workflow that produces almost all the movement here never opens a
# board: an agent picks an issue up, works it, pushes, and closes it. The row
# has to say so anyway (issue #254).
test "a session working an issue renders the started arc with no board", %{conn: conn} do
{:ok, started} = Issues.create_issue(repository(), %{"title" => "Being worked"})
{:ok, queued} = Issues.create_issue(repository(), %{"title" => "Nobody has touched this"})
# The reader is the account the setup logged in: the owner running the
# session is the one looking at the list.
{:ok, _thread} =
OpenAgents.Threads.open(github_user("issue-index"), "Work it", issue_id: started.id)
{:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
assert has_element?(
view,
~s{#issues-#{started.id} .issue-status[data-category="started"] .issue-status__arc}
)
assert has_element?(view, ~s{#issues-#{queued.id} .issue-status[data-category="open"]})
refute has_element?(view, ~s{#issues-#{queued.id} .issue-status[data-category="started"]})
end
defp repository do
OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
end
end