Files
firehose/app/test/firehose_web/controllers/blog_test.exs
T
Willem van den Ende e3fc734862 External links: screen-reader hint on links that leave the blog
External links in post bodies already opened in a new tab and showed an
icon, but the icon is invisible to screen readers, so nothing announced
that the link leaves the site.

- blogex: ExternalLinks.rewrite/1 appends
  <span class="blogex-external-hint"> (opens in a new tab)</span> as the
  last child of every external anchor. No aria-label, so the accessible
  name still contains the visible link text (WCAG 2.5.3); the icon colour
  and 2.5.3 both hold. Idempotent: a second pass adds no second hint.
- blogex: anchors are now matched together with their closing tag
  (the hint goes inside), so a stray unclosed <a> is left alone.
- app: sr-only rule for .blogex-external-hint in .blogex-post-body. The
  split stays as before: blogex marks up, the host app styles.
- tests: hint cases in blogex (appended, per link, after inner markup,
  idempotent, not on internal/mailto/anchor links) + anchors without href
  (Blogex.ExternalLinks now at 100% line coverage), and an app test that
  the hint reaches the rendered post page. Two exact-string expectations
  updated for the intended markup change.

make test: app 179, blogex 162. make verify: green.
2026-10-26 20:28:16 +00:00

196 lines
7.0 KiB
Elixir

# Firehose blog controller tests
defmodule FirehoseWeb.BlogTest do
use FirehoseWeb.ConnCase
defp visit_blog_page(conn, blog_id, suffix \\ "") do
path = "/blog/#{blog_id}" <> suffix
body = conn |> get(path) |> html_response(200)
body
end
describe "engineering blog (HTML)" do
test "GET /blog/engineering returns HTML index with layout", %{conn: conn} do
body = visit_blog_page(conn, "engineering")
assert body =~ "Engineering Blog"
assert body =~ "firehose"
end
test "GET /blog/engineering/:slug returns HTML post with layout", %{conn: conn} do
body = visit_blog_page(conn, "engineering", "/hello-world")
assert body =~ "Hello World"
end
end
describe "release notes blog (HTML)" do
test "GET /blog/releases returns HTML index", %{conn: conn} do
body = visit_blog_page(conn, "releases")
assert body =~ "Release Notes"
assert body =~ "v0.1.0 Released"
end
test "GET /blog/releases/:slug returns HTML post", %{conn: conn} do
body = visit_blog_page(conn, "releases", "/v0-1-0")
assert body =~ "v0.1.0 Released"
end
end
describe "external links in compiled posts" do
# The marker markup is applied at compile time by Blogex.ExternalLinks
# (blogex/test/blogex/external_links_test.exs covers the rewrite itself);
# these tests verify the real compiled posts make it to the page.
defp latest_post_with_links,
do: Firehose.EngineeringBlog.get_post!("delayed-gratification-in-ai-summer")
test "wikipedia link carries target, rel and marker class in compiled body" do
post = latest_post_with_links()
assert post.body =~
~s(<a href="https://en.wikipedia.org/wiki/Attention_Is_All_You_Need" target="_blank" rel="noopener" class="blogex-external">)
end
test "every external anchor in compiled body is marked up" do
post = latest_post_with_links()
anchors = Regex.scan(~r/<a\s[^>]*>/, post.body) |> List.flatten()
external_anchors =
Enum.filter(anchors, &(&1 =~ ~r<href="https?://>))
assert length(external_anchors) > 1
assert Enum.all?(external_anchors, fn anchor ->
anchor =~ ~s(target="_blank") and
anchor =~ ~s(rel="noopener") and
anchor =~ ~s(class="blogex-external")
end)
end
test "internal links in compiled body are untouched" do
post = latest_post_with_links()
assert post.body =~
~s(<a href="/blog/engineering/swim-the-test">Swim the Test</a>)
end
test "rendered post page shows external links opening in a new tab", %{conn: conn} do
body =
conn |> get("/blog/engineering/delayed-gratification-in-ai-summer") |> html_response(200)
assert body =~ "target=\"_blank\" rel=\"noopener\" class=\"blogex-external\""
end
test "rendered post page carries the screen-reader hint for external links", %{conn: conn} do
body =
conn |> get("/blog/engineering/delayed-gratification-in-ai-summer") |> html_response(200)
assert body =~ ~s|<span class="blogex-external-hint"> (opens in a new tab)</span>|
end
end
describe "input validation" do
test "GET /blog/nonexistent returns 404", %{conn: conn} do
assert conn |> get("/blog/nonexistent") |> html_response(404)
end
test "GET /blog/engineering?page=abc falls back to page 1", %{conn: conn} do
assert conn |> get("/blog/engineering?page=abc") |> html_response(200) =~ "Engineering Blog"
end
test "GET /blog/engineering?page=-1 falls back to page 1", %{conn: conn} do
assert conn |> get("/blog/engineering?page=-1") |> html_response(200) =~ "Engineering Blog"
end
test "GET /blog/engineering?page=0 falls back to page 1", %{conn: conn} do
assert conn |> get("/blog/engineering?page=0") |> html_response(200) =~ "Engineering Blog"
end
test "GET /blog/engineering/nonexistent-post returns 404", %{conn: conn} do
assert_raise Blogex.NotFoundError, fn ->
get(conn, "/blog/engineering/nonexistent-post")
end
end
end
describe "engineering blog (JSON API)" do
test "GET /api/blog/engineering returns post index", %{conn: conn} do
assert %{"blog" => "engineering", "posts" => posts} =
conn
|> put_req_header("accept", "application/json")
|> get("/api/blog/engineering")
|> json_response(200)
assert is_list(posts)
refute Enum.empty?(posts)
end
test "GET /api/blog/engineering/:slug returns a post", %{conn: conn} do
assert %{"id" => "hello-world", "title" => "Hello World"} =
conn
|> put_req_header("accept", "application/json")
|> get("/api/blog/engineering/hello-world")
|> json_response(200)
end
test "GET /api/blog/engineering/:slug returns 404 for missing post", %{conn: conn} do
assert conn
|> put_req_header("accept", "application/json")
|> get("/api/blog/engineering/nonexistent")
|> response(404)
end
test "GET /api/blog/engineering/feed.xml returns RSS", %{conn: conn} do
response = conn |> get("/api/blog/engineering/feed.xml")
assert response(response, 200) =~ "<rss"
assert response_content_type(response, :xml)
end
test "GET /api/blog/engineering/tag/:tag returns JSON with posts", %{conn: conn} do
assert %{"blog" => "engineering", "tag" => "elixir", "posts" => posts} =
conn
|> put_req_header("accept", "application/json")
|> get("/api/blog/engineering/tag/elixir")
|> json_response(200)
assert is_list(posts)
end
end
describe "release notes blog (JSON API)" do
test "GET /api/blog/releases returns post index", %{conn: conn} do
assert %{"blog" => "release_notes", "posts" => posts} =
conn
|> put_req_header("accept", "application/json")
|> get("/api/blog/releases")
|> json_response(200)
assert is_list(posts)
refute Enum.empty?(posts)
end
test "GET /api/blog/releases/:slug returns a post", %{conn: conn} do
assert %{"id" => "v0-1-0", "title" => "v0.1.0 Released"} =
conn
|> put_req_header("accept", "application/json")
|> get("/api/blog/releases/v0-1-0")
|> json_response(200)
end
test "GET /api/blog/releases/feed.xml returns RSS", %{conn: conn} do
response = conn |> get("/api/blog/releases/feed.xml")
assert response(response, 200) =~ "<rss"
assert response_content_type(response, :xml)
end
test "GET /api/blog/releases/tag/:tag returns JSON with posts", %{conn: conn} do
assert %{"blog" => "release_notes", "tag" => "elixir", "posts" => posts} =
conn
|> put_req_header("accept", "application/json")
|> get("/api/blog/releases/tag/elixir")
|> json_response(200)
assert is_list(posts)
end
end
end