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:
@@ -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 =~ "<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(<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
|
||||
@@ -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 =~ "<p>"
|
||||
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}">← 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 =~ "<p>"
|
||||
refute html =~ "<strong>"
|
||||
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
|
||||
@@ -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 =~ "<p>"
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user