test/openagents_web/icons_test.exs

58e6347eeb72 · 4 KB

defmodule OpenAgentsWeb.IconsTest do
  @moduledoc """
  Contract tests for the vendored icon set.

  The set is generated by `mix openagents.icons.vendor`, so these guard the
  conversion and the glyphs the product depends on: a bad re-vendor must fail
  the build rather than silently emptying an affordance.
  """

  use ExUnit.Case, async: true
  import Phoenix.Component
  import Phoenix.LiveViewTest
  import OpenAgentsWeb.UI

  alias OpenAgentsWeb.Icons

  # Every glyph a OpenAgents surface renders. If a re-vendor drops one of these, the
  # control loses its affordance, so name them explicitly.
  @in_use ~w(
    arrow-up stop history memory-on-remember arrow-left chevron-down logout
    download trash x mic mic-off sound-on-read-out-loud-speaker x-circle
    enter-login trophy-top shield-lock sidebar-collapse-left sidebar-open-left
    menu
  )

  describe "the vendored set" do
    test "is present and complete" do
      # 755 vendored glyphs plus the brand marks, which live apart because a
      # logo cannot come from a generic set and because the vendoring task
      # clears priv/icons on every run, plus the octicon state glyphs in
      # priv/octicons, which are governed by their own pinned upstream.
      {brand, rest} = Enum.split_with(Icons.names(), &String.starts_with?(&1, "brand-"))
      {octicons, vendored} = Enum.split_with(rest, &String.starts_with?(&1, "octicon-"))

      assert length(vendored) == 755
      assert brand != []
      assert "brand-github" in brand
      assert octicons == ["octicon-issue-closed", "octicon-issue-opened"]
      assert length(Icons.names()) == Icons.count()
    end

    test "contains every glyph the product depends on" do
      for name <- @in_use do
        assert Icons.exists?(name), "missing icon #{name}; a re-vendor may have dropped it"
      end
    end

    test "every glyph is a single svg element carrying a viewBox" do
      for name <- Icons.names() do
        {view_box, inner} = Icons.fetch!(name)

        assert view_box =~ ~r/^[\d.\s-]+$/, "#{name} has a malformed viewBox: #{view_box}"
        assert inner != "", "#{name} has no markup"
        refute inner =~ "<svg", "#{name} contains a nested svg"
      end
    end

    test "no glyph carries leftover JSX from the conversion" do
      for name <- Icons.names() do
        {_view_box, inner} = Icons.fetch!(name)

        refute inner =~ "{...props}", "#{name} kept its props spread"
        refute inner =~ ~r/=\{/, "#{name} kept a JSX expression value"
        refute inner =~ ~r/\b(fillRule|clipRule|strokeWidth|className)=/, "#{name} kept camelCase"
      end
    end

    test "viewBoxes are preserved per glyph rather than assumed to be 24" do
      # 25 upstream glyphs are not 24x24. Hardcoding would distort them.
      view_boxes = Enum.map(Icons.names(), fn name -> Icons.fetch!(name) |> elem(0) end)

      assert "0 0 24 24" in view_boxes
      assert Enum.any?(view_boxes, &(&1 != "0 0 24 24"))
    end

    test "unknown names raise with a pointer to the vendoring task" do
      error = assert_raise ArgumentError, fn -> Icons.fetch!("definitely-not-an-icon") end

      assert error.message =~ "unknown icon"
      assert error.message =~ "OpenAgentsWeb.Icons.names/0"
    end
  end

  describe "icon/1" do
    test "renders inline svg inheriting size and colour from its surroundings" do
      assigns = %{}

      html = rendered_to_string(~H|<.icon name="arrow-up" />|)

      assert html =~ ~s(viewBox="0 0 24 24")
      assert html =~ ~s(width="1em")
      assert html =~ ~s(height="1em")
      assert html =~ ~s(fill="currentColor")
      assert html =~ "<path"
    end

    test "is decorative by default, because an adjacent word usually names the control" do
      assigns = %{}

      html = rendered_to_string(~H|<.icon name="trash" />|)

      assert html =~ ~s(aria-hidden="true")
      assert html =~ ~s(focusable="false")
      refute html =~ ~s(role="img")
    end

    test "takes an accessible name when the glyph is the whole message" do
      assigns = %{}

      html = rendered_to_string(~H|<.icon name="mic" label="Microphone" />|)

      assert html =~ ~s(role="img")
      assert html =~ ~s(aria-label="Microphone")
      refute html =~ "aria-hidden"
    end

    test "carries the per-glyph viewBox rather than a fixed one" do
      assigns = %{}
      odd = Enum.find(Icons.names(), &(Icons.fetch!(&1) |> elem(0) != "0 0 24 24"))
      assigns = Map.put(assigns, :name, odd)

      html = rendered_to_string(~H|<.icon name={@name} />|)

      assert html =~ ~s(viewBox="#{Icons.fetch!(odd) |> elem(0)}")
    end

    test "accepts a class without losing its own" do
      assigns = %{}

      html = rendered_to_string(~H|<.icon name="x" class="mr-1" />|)

      assert html =~ "icon"
      assert html =~ "mr-1"
    end

    test "raises on an unknown glyph instead of rendering an empty box" do
      assigns = %{}

      assert_raise ArgumentError, ~r/unknown icon/, fn ->
        rendered_to_string(~H|<.icon name="not-a-real-icon" />|)
      end
    end
  end
end