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