test/openagents_web/live/home_dashboard_live_test.exs

main at 58e6347eeb72 · 6 KB

defmodule OpenAgentsWeb.HomeDashboardLiveTest do
  @moduledoc """
  The signed-in homepage (#128, superseding #26).

  What matters here is that the dashboard describes the viewer's own work
  across every repository they can read rather than one repository picked for
  them, that its counts agree with `/issues` and `/projects` because they are
  read the same way, that a repository the viewer cannot read is absent from
  both the rows and the numbers, and that each kind of emptiness explains
  itself.
  """

  use OpenAgentsWeb.ConnCase, async: false

  import Ecto.Query
  import Phoenix.LiveViewTest

  alias OpenAgents.Issues
  alias OpenAgents.Projects
  alias OpenAgents.Repo
  alias OpenAgents.Repositories
  alias OpenAgents.Repositories.Membership
  alias OpenAgents.Repositories.Repository

  setup %{conn: conn} do
    owner = github_user("home-dashboard-owner")

    # Private with a membership, so the same seeding proves both halves: the
    # owner reads across both repositories, and a stranger reads neither.
    first = ready_repository!(owner, "first-repository", "private")
    second = ready_repository!(owner, "second-repository", "private")

    {:ok, alpha} = Issues.create_issue(first, %{"title" => "Alpha needs a decision"}, owner)
    {:ok, beta} = Issues.create_issue(second, %{"title" => "Beta needs a decision"}, owner)

    board = project!(second, owner, "Second quarter board")

    %{
      conn: Plug.Test.init_test_session(conn, %{"user_id" => owner.id}),
      owner: owner,
      first: first,
      second: second,
      alpha: alpha,
      beta: beta,
      board: board
    }
  end

  test "the feed carries issues from every repository you can read", context do
    {:ok, view, html} = live(context.conn, ~p"/")

    assert html =~ "Alpha needs a decision"
    assert html =~ "Beta needs a decision"

    # Each row navigates to that issue in its own repository, and names it,
    # because a cross-repository feed is unreadable without the repository.
    assert issue_linked?(view, context.first, context.alpha)
    assert issue_linked?(view, context.second, context.beta)
    assert html =~ "#{context.first.owner}/#{context.first.name}"
  end

  test "the issue counts are the ones /issues shows the same viewer", context do
    {:ok, closable} = Issues.create_issue(context.first, %{"title" => "Already done"})
    {:ok, _closed} = Issues.update_issue(closable, %{"state" => "closed"}, context.owner)

    {:ok, home, _html} = live(context.conn, ~p"/")

    open = integer_at(home, "#dashboard-open-issue-count")
    closed = integer_at(home, "#dashboard-closed-issue-count")

    assert open == 2
    assert closed == 1

    {:ok, workspace, _html} = live(context.conn, ~p"/issues")

    assert has_element?(workspace, ~s{a[href="/issues?state=open"]}, "#{open} Open")
    assert has_element?(workspace, ~s{a[href="/issues?state=closed"]}, "#{closed} Closed")
  end

  test "the project panel and its count are the ones /projects shows", context do
    {:ok, view, html} = live(context.conn, ~p"/")

    assert html =~ "Second quarter board"

    assert has_element?(
             view,
             ~s{a[href="/#{context.second.owner}/#{context.second.name}/projects/#{context.board.number}"]}
           )

    open = integer_at(view, "#dashboard-open-project-count")
    assert open == 1

    {:ok, workspace, _html} = live(context.conn, ~p"/projects")
    assert has_element?(workspace, ~s{a[href="/projects?state=open"]}, "#{open} Open")
  end

  test "the panels point at the workspace lists, not at one repository", context do
    {:ok, view, _html} = live(context.conn, ~p"/")

    assert has_element?(view, ~s{a[href="/issues"]}, "View all")
    assert has_element?(view, ~s{a[href="/projects"]}, "View all")

    refute has_element?(
             view,
             ~s{a[href="/#{context.first.owner}/#{context.first.name}/issues"]}
           )
  end

  test "a stranger sees neither the rows nor the counts of a repository they cannot read",
       context do
    stranger = github_user("home-dashboard-stranger")
    conn = Plug.Test.init_test_session(build_conn(), %{"user_id" => stranger.id})

    {:ok, view, html} = live(conn, ~p"/")

    refute html =~ "Alpha needs a decision"
    refute html =~ "Beta needs a decision"
    refute html =~ "Second quarter board"
    refute issue_linked?(view, context.first, context.alpha)
    refute issue_linked?(view, context.second, context.beta)

    assert integer_at(view, "#dashboard-open-issue-count") ==
             Issues.count_visible_issues(stranger, state: "open")

    assert integer_at(view, "#dashboard-open-issue-count") <
             Issues.count_visible_issues(context.owner, state: "open")

    assert integer_at(view, "#dashboard-open-project-count") ==
             Projects.count_visible_projects(stranger, state: "open")
  end

  test "a viewer with readable repositories and no issues is told exactly that", %{} do
    stranger = github_user("home-dashboard-quiet")
    conn = Plug.Test.init_test_session(build_conn(), %{"user_id" => stranger.id})

    {:ok, view, html} = live(conn, ~p"/")

    assert integer_at(view, "#dashboard-open-issue-count") == 0
    assert html =~ "No open issues in the repositories you can read."
    assert html =~ "No open projects in the repositories you can read."
    refute html =~ "Import your first repository"
  end

  test "a viewer who can read no repository is told that instead", %{} do
    stranger = github_user("home-dashboard-repoless")

    Repo.delete_all(from membership in Membership, where: membership.user_id == ^stranger.id)
    Repo.update_all(Repository, set: [visibility: "private"])

    conn = Plug.Test.init_test_session(build_conn(), %{"user_id" => stranger.id})
    {:ok, _view, html} = live(conn, ~p"/")

    assert html =~ "No repositories yet."
    assert html =~ "Import your first repository to start tracking issues."
    refute html =~ "No open issues in the repositories you can read."
  end

  defp integer_at(view, selector) do
    view
    |> render()
    |> LazyHTML.from_fragment()
    |> LazyHTML.query(selector)
    |> LazyHTML.text()
    |> String.trim()
    |> String.to_integer()
  end

  defp issue_linked?(view, repository, issue) do
    has_element?(
      view,
      ~s{a[href="/#{repository.owner}/#{repository.name}/issues/#{issue.number}"]}
    )
  end

  defp project!(repository, owner, title) do
    {:ok, project} =
      Projects.create_project(
        repository,
        %{title: title, owner: owner.github_login, state: "open"},
        owner
      )

    project
  end

  defp ready_repository!(owner, name, visibility) do
    {:ok, repository, :created} =
      Repositories.create_user_repository(
        owner,
        %{name: name, visibility: visibility},
        "#{name}-key"
      )

    repository
    |> Ecto.Changeset.change(lifecycle_state: "ready", ready_at: DateTime.utc_now())
    |> Repo.update!()
  end
end