diff --git a/blogex/mix.exs b/blogex/mix.exs
index 7e94fbb..23ba5e4 100644
--- a/blogex/mix.exs
+++ b/blogex/mix.exs
@@ -56,22 +56,22 @@ defmodule Blogex.MixProject do
# in blogex/). Rule: the threshold is a FLOOR. Raise it as behavior-test yaks
# land; never lower it. A drop means the change (or an exclusion) is wrong.
#
- # Measured 2026-10-28, with the test scaffolding below excluded: 78.59% — the
+ # Measured 2026-10-28, with the test scaffolding below excluded: 99.69% — the
# same via `mix test --cover` and via `mix test --cover --export-coverage
# default` + `mix test.coverage`. Floor is the integer below that.
#
# Excluded: the test scaffolding in `ignore_modules` below. The two registry
# blog doubles (Blogex.Test.AlphaBlog/BetaBlog) are deliberately not listed
- # there: they are fully exercised, and excluding them measures 78.06% — the
- # 78 floor holds either way.
+ # there: they are fully exercised, and excluding them measures 99.69% too —
+ # the 99 floor holds either way.
#
- # Everything in lib/blogex counts, and today's gaps are real:
- # Blogex.Components and Blogex.Layout are at 0% because the router tests only
- # exercise the JSON responses — covering the rendered HTML is its own yak.
- # Blogex.Router (72.06%) still has its HTML arms and `match _` uncovered.
+ # Everything in lib/blogex counts, and one gap is left:
+ # Blogex.LinkError is at 88.89% (8 of 9 lines). The rendered-HTML behavior
+ # tests closed the rest: Blogex.Components and Blogex.Layout went from 0% to
+ # 100%, and Blogex.Router now covers its browser arms and `match _`.
defp test_coverage do
[
- summary: [threshold: 78],
+ summary: [threshold: 99],
ignore_modules: [
Blogex.Test.FakeBlog,
Blogex.Test.PostBuilder,
diff --git a/blogex/test/blogex/components_test.exs b/blogex/test/blogex/components_test.exs
new file mode 100644
index 0000000..0c64289
--- /dev/null
+++ b/blogex/test/blogex/components_test.exs
@@ -0,0 +1,223 @@
+defmodule Blogex.ComponentsTest do
+ use ExUnit.Case, async: true
+
+ import Phoenix.LiveViewTest
+ import Blogex.Test.PostBuilder
+
+ @base_path "/blog/engineering"
+
+ describe "post_index/1" do
+ test "links each post title to its own page under base_path" do
+ html =
+ render_post_index([
+ build(id: "first-post", title: "First post"),
+ build(id: "second-post", title: "Second post")
+ ])
+
+ assert html =~ ~s(href="#{@base_path}/first-post")
+ assert html =~ ">First post"
+ assert html =~ ~s(href="#{@base_path}/second-post")
+ assert html =~ ">Second post"
+ end
+
+ test "shows each post's description" do
+ html =
+ render_post_index([
+ build(id: "processes-post", description: "Everything about processes"),
+ build(id: "supervision-post", description: "Everything about supervision")
+ ])
+
+ assert html =~ "Everything about processes"
+ assert html =~ "Everything about supervision"
+ end
+
+ test "shows date, author and tags for each post" do
+ html =
+ render_post_index([
+ build(
+ id: "authored-post",
+ date: ~D[2026-03-10],
+ author: "Ada Lovelace",
+ tags: ["elixir"]
+ )
+ ])
+
+ assert html =~ "March 10, 2026"
+ assert html =~ "by Ada Lovelace"
+ assert html =~ ~s(href="#{@base_path}/tag/elixir")
+ end
+ end
+
+ describe "post_show/1" do
+ test "renders the post title as the page heading" do
+ html = render_post_show(build(id: "heading-post", title: "Rendering Rendered HTML"))
+
+ assert html =~ "
Rendering Rendered HTML
"
+ end
+
+ test "renders the body as markup, not escaped text" do
+ html =
+ render_post_show(build(id: "markup-post", body: "
Hello world
"))
+
+ assert html =~ "
Hello world
"
+ refute html =~ "<p>"
+ refute html =~ "<strong>"
+ end
+
+ test "shows the post's date, author and tags" do
+ html =
+ render_post_show(
+ build(
+ id: "meta-post",
+ date: ~D[2026-03-10],
+ author: "Grace Hopper",
+ tags: ["compilers", "navy"]
+ )
+ )
+
+ assert html =~ ~s(