yak(behavior-tests-blogex-rendered-html-components-layout-router-html-0dev): behavior tests: blogex rendered HTML (components, layout, router HTML)

This commit is contained in:
Willem van den Ende
2026-10-28 22:21:23 +00:00
parent cc03cefe8d
commit 44a343ae72
4 changed files with 446 additions and 8 deletions
+223
View File
@@ -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</a>"
assert html =~ ~s(href="#{@base_path}/second-post")
assert html =~ ">Second post</a>"
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 =~ "<h1>Rendering Rendered HTML</h1>"
end
test "renders the body as markup, not escaped text" do
html =
render_post_show(build(id: "markup-post", body: "<p>Hello <strong>world</strong></p>"))
assert html =~ "<p>Hello <strong>world</strong></p>"
refute html =~ "&lt;p&gt;"
refute html =~ "&lt;strong&gt;"
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(<time datetime="2026-03-10">)
assert html =~ "March 10, 2026"
assert html =~ "by Grace Hopper"
assert html =~ ~s(href="#{@base_path}/tag/compilers")
assert html =~ ~s(href="#{@base_path}/tag/navy")
end
end
describe "post_meta/1" do
test "renders the date as a machine-readable time element and a human date" do
html = render_post_meta(build(date: ~D[2026-03-10]))
assert html =~ ~s(<time datetime="2026-03-10">)
assert html =~ "March 10, 2026"
end
test "shows the author when the post has one" do
html = render_post_meta(build(author: "Ada Lovelace"))
assert html =~ ~s(<span class="blogex-post-author">)
assert html =~ "by Ada Lovelace"
end
test "has no author line when the post has no author" do
html = render_post_meta(build(author: nil))
refute html =~ "blogex-post-author"
refute html =~ "by "
end
test "links every tag under base_path" do
html = render_post_meta(build(tags: ["elixir", "otp"]))
assert html =~ ~s(href="#{@base_path}/tag/elixir")
assert html =~ ~s(href="#{@base_path}/tag/otp")
end
test "marks only the current tag as active" do
html = render_post_meta(build(tags: ["elixir", "otp"]), current_tag: "elixir")
assert occurrences(html, "blogex-tag-active") == 1
assert [_, after_active] =
String.split(html, ~s(class="blogex-tag-link blogex-tag-active"))
assert after_active =~ ~r/^>\s*elixir\s*<\/a>/
end
end
describe "tag_list/1" do
test "links every tag under base_path" do
html = render_tag_list(["elixir", "otp"])
assert html =~ ~s(href="#{@base_path}/tag/elixir")
assert html =~ ~s(href="#{@base_path}/tag/otp")
end
test "marks only the current tag as active" do
html = render_tag_list(["elixir", "otp"], current_tag: "elixir")
assert occurrences(html, "blogex-tag-active") == 1
assert [_, after_active] =
String.split(html, ~s(class="blogex-tag-link blogex-tag-active"))
assert after_active =~ ~r/^>\s*elixir\s*<\/a>/
end
end
describe "pagination/1" do
test "renders nothing when there is only one page" do
assert render_pagination(page: 1, total_pages: 1) |> String.trim() == ""
end
test "on the first of several pages there is an Older link and no Newer link" do
html = render_pagination(page: 1, total_pages: 3)
assert html =~ "Page 1 of 3"
assert html =~ ~s(href="#{@base_path}?page=2")
assert html =~ "Older"
refute html =~ "Newer"
end
test "on a middle page there are both Newer and Older links" do
html = render_pagination(page: 2, total_pages: 3)
assert html =~ "Page 2 of 3"
assert html =~ ~s(href="#{@base_path}?page=1")
assert html =~ ~s(href="#{@base_path}?page=3")
assert html =~ "Newer"
assert html =~ "Older"
end
test "on the last page there is a Newer link and no Older link" do
html = render_pagination(page: 3, total_pages: 3)
assert html =~ "Page 3 of 3"
assert html =~ ~s(href="#{@base_path}?page=2")
assert html =~ "Newer"
refute html =~ "Older"
end
end
defp render_post_index(posts, current_tag \\ nil) do
render_component(&Blogex.Components.post_index/1,
posts: posts,
base_path: @base_path,
current_tag: current_tag
)
end
defp render_post_show(post) do
render_component(&Blogex.Components.post_show/1, post: post, base_path: @base_path)
end
defp render_post_meta(post, opts \\ []) do
render_component(&Blogex.Components.post_meta/1,
post: post,
base_path: @base_path,
current_tag: opts[:current_tag]
)
end
defp render_tag_list(tags, opts \\ []) do
render_component(&Blogex.Components.tag_list/1,
tags: tags,
base_path: @base_path,
current_tag: opts[:current_tag]
)
end
defp render_pagination(opts) do
render_component(&Blogex.Components.pagination/1,
page: opts[:page],
total_pages: opts[:total_pages],
base_path: @base_path
)
end
defp occurrences(html, token) do
html
|> String.split(token)
|> length()
|> Kernel.-(1)
end
end
+132
View File
@@ -0,0 +1,132 @@
defmodule Blogex.LayoutTest do
use ExUnit.Case, async: true
import Phoenix.LiveViewTest
import Blogex.Test.PostBuilder
@base_path "/blog/engineering"
@blog_title "Engineering Blog"
@blog_description "Notes from the platform team"
describe "page/1" do
test "titles the document and wraps the inner content" do
html = render_page("A document title")
assert html =~ "<!DOCTYPE html>"
assert html =~ ~s(<html lang="en">)
assert html =~ "<title>A document title</title>"
end
test "renders inner_content as markup, not escaped text" do
html = render_page("A document title", Phoenix.HTML.raw("<p>inner</p>"))
assert html =~ "<p>inner</p>"
refute html =~ "&lt;p&gt;"
end
end
describe "index_page/1" do
test "shows the blog title and description" do
html = render_index_page([])
assert html =~ "<h1>#{@blog_title}</h1>"
assert html =~ @blog_description
end
test "lists the posts with their titles linked" do
html = render_index_page([build(id: "first-post", title: "First post")])
assert html =~ ~s(href="#{@base_path}/first-post")
assert html =~ ">First post</a>"
end
test "shows no pagination when the blog has a single page" do
html = render_index_page([build(id: "only-post")], page: 1, total_pages: 1)
refute html =~ "Older"
refute html =~ "Newer"
refute html =~ "Page 1 of 1"
end
test "shows pagination when there is more than one page" do
html = render_index_page([build(id: "paged-post")], page: 2, total_pages: 3)
assert html =~ "Page 2 of 3"
end
end
describe "show_page/1" do
test "uses the post title as the document title" do
post = build(id: "titled-post", title: "A Post About Layout")
assert render_show_page(post) =~ "<title>#{post.title}</title>"
end
test "offers a link back to the blog index" do
html = render_show_page(build())
assert html =~ ~s(<a href="#{@base_path}">&larr; Back</a>)
end
test "renders the post body as markup, not escaped text" do
html = render_show_page(build(body: "<p>Hello <strong>world</strong></p>"))
assert html =~ "<p>Hello <strong>world</strong></p>"
refute html =~ "&lt;p&gt;"
refute html =~ "&lt;strong&gt;"
end
end
describe "tag_page/1" do
test "titles the page with the blog title and the tag" do
html = render_tag_page(tag: "elixir", posts: [])
assert html =~ "<title>#{@blog_title} — #elixir</title>"
end
test "announces the tag and lists the posts it was given" do
html =
render_tag_page(
tag: "elixir",
posts: [
build(id: "tagged-post", title: "Tagged post"),
build(id: "another-tagged-post", title: "Another tagged post")
]
)
assert html =~ ~s(<h1>Posts tagged "elixir"</h1>)
assert html =~ ~s(href="#{@base_path}/tagged-post")
assert html =~ ">Tagged post</a>"
assert html =~ ~s(href="#{@base_path}/another-tagged-post")
assert html =~ ">Another tagged post</a>"
end
end
defp render_page(title, inner_content \\ Phoenix.HTML.raw("<p>inner</p>")) do
render_component(&Blogex.Layout.page/1, title: title, inner_content: inner_content)
end
defp render_index_page(posts, opts \\ []) do
render_component(&Blogex.Layout.index_page/1,
blog_title: @blog_title,
blog_description: @blog_description,
posts: posts,
base_path: @base_path,
page: Keyword.get(opts, :page, 1),
total_pages: Keyword.get(opts, :total_pages, 1)
)
end
defp render_show_page(post) do
render_component(&Blogex.Layout.show_page/1, post: post, base_path: @base_path)
end
defp render_tag_page(opts) do
render_component(&Blogex.Layout.tag_page/1,
blog_title: @blog_title,
tag: opts[:tag],
posts: opts[:posts],
base_path: @base_path
)
end
end
+83
View File
@@ -1,6 +1,7 @@
defmodule Blogex.RouterTest do
use ExUnit.Case
import Plug.Test
import Plug.Conn, only: [put_req_header: 3]
import Blogex.Test.PostBuilder
alias Blogex.Test.FakeBlog
@@ -35,6 +36,18 @@ defmodule Blogex.RouterTest do
|> Blogex.Router.call(Blogex.Router.init(blog: FakeBlog))
end
defp call_html(path) do
conn(:get, path)
|> put_req_header("accept", "text/html")
|> Blogex.Router.call(Blogex.Router.init(blog: FakeBlog))
end
defp call_json(path) do
conn(:get, path)
|> put_req_header("accept", "application/json")
|> Blogex.Router.call(Blogex.Router.init(blog: FakeBlog))
end
describe "GET /feed.xml" do
test "returns RSS XML" do
conn = call(:get, "/feed.xml")
@@ -153,6 +166,76 @@ defmodule Blogex.RouterTest do
end
end
# The browser arms were previously unexercised: every test above goes through
# `call/2`, which sends no Accept header, so `wants_html?/1` was always false.
describe "GET / (browser)" do
test "returns an HTML page with the blog title and the published posts" do
conn = call_html("/")
assert conn.status == 200
assert get_content_type(conn) =~ "text/html"
assert conn.resp_body =~ "Test Blog"
assert conn.resp_body =~ "First"
assert conn.resp_body =~ ~s(href="/blog/test/first-post")
refute conn.resp_body =~ "future-post"
end
test "leaves JSON clients alone" do
conn = call_json("/")
assert get_content_type(conn) =~ "application/json"
body = Jason.decode!(conn.resp_body)
assert body["title"] == "Test Blog"
assert body["total_entries"] == 2
end
end
describe "GET /:slug (browser)" do
test "returns the post as an HTML page" do
conn = call_html("/first-post")
assert conn.status == 200
assert get_content_type(conn) =~ "text/html"
assert conn.resp_body =~ "First"
assert conn.resp_body =~ "<p>Post body.</p>"
refute conn.resp_body =~ "&lt;p&gt;"
end
test "leaves JSON clients alone" do
conn = call_json("/first-post")
assert get_content_type(conn) =~ "application/json"
assert Jason.decode!(conn.resp_body)["id"] == "first-post"
end
end
describe "GET /tag/:tag (browser)" do
test "returns HTML with only the posts carrying that tag" do
conn = call_html("/tag/elixir")
assert conn.status == 200
assert get_content_type(conn) =~ "text/html"
assert conn.resp_body =~ "First"
refute conn.resp_body =~ "Second"
end
test "leaves JSON clients alone" do
conn = call_json("/tag/elixir")
assert get_content_type(conn) =~ "application/json"
assert Jason.decode!(conn.resp_body)["tag"] == "elixir"
end
end
describe "unknown routes" do
test "answers 404 Not found" do
conn = call(:post, "/whatever")
assert conn.status == 404
assert conn.resp_body == "Not found"
end
end
describe "Helpers.base_url/1" do
# `port_suffix/2` has one clause per (scheme, default port) pair plus the
# catch-all, and the feeds embed this URL. All four shapes are pinned so a