berrypod/test/berrypod/pages_test.exs
jamey 32f54c7afc add generic page renderer with block dispatch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:07:57 +00:00

348 lines
10 KiB
Elixir

defmodule Berrypod.PagesTest do
use Berrypod.DataCase, async: false
alias Berrypod.Pages
alias Berrypod.Pages.{Page, BlockTypes, Defaults, PageCache}
setup do
# Clear cached pages between tests so save_page side effects don't leak
PageCache.invalidate_all()
:ok
end
describe "get_page/1" do
test "returns defaults when nothing in DB" do
page = Pages.get_page("home")
assert page.slug == "home"
assert page.title == "Home page"
assert is_list(page.blocks)
assert length(page.blocks) == 4
types = Enum.map(page.blocks, & &1["type"])
assert types == ["hero", "category_nav", "featured_products", "image_text"]
end
test "returns DB version when page has been saved" do
{:ok, _} =
Pages.save_page("home", %{
title: "Custom home",
blocks: [%{"id" => "blk_test", "type" => "hero", "settings" => %{}}]
})
page = Pages.get_page("home")
assert page.slug == "home"
assert page.title == "Custom home"
assert length(page.blocks) == 1
end
test "each block has an id, type, and settings" do
page = Pages.get_page("contact")
for block <- page.blocks do
assert is_binary(block["id"]), "block missing id"
assert is_binary(block["type"]), "block missing type"
assert is_map(block["settings"]), "block missing settings"
end
end
end
describe "list_pages/0" do
test "returns all 14 pages" do
pages = Pages.list_pages()
assert length(pages) == 14
slugs = Enum.map(pages, & &1.slug)
assert "home" in slugs
assert "pdp" in slugs
assert "error" in slugs
end
end
describe "save_page/2" do
test "creates a new page in the DB" do
blocks = [%{"id" => "blk_1", "type" => "hero", "settings" => %{"title" => "Hello"}}]
assert {:ok, page} = Pages.save_page("about", %{title: "About us", blocks: blocks})
assert page.slug == "about"
assert page.title == "About us"
assert length(page.blocks) == 1
end
test "updates an existing page" do
{:ok, _} = Pages.save_page("about", %{title: "About v1", blocks: []})
{:ok, page} = Pages.save_page("about", %{title: "About v2", blocks: []})
assert page.title == "About v2"
assert Repo.aggregate(from(p in Page, where: p.slug == "about"), :count) == 1
end
test "invalidates cache on save" do
# Prime the cache
_page = Pages.get_page("home")
# Save a new version
{:ok, _} = Pages.save_page("home", %{title: "New home", blocks: []})
# Should return the new version
page = Pages.get_page("home")
assert page.title == "New home"
end
test "rejects invalid slug" do
assert {:error, changeset} =
Pages.save_page("nonexistent", %{title: "Bad", blocks: []})
assert errors_on(changeset).slug
end
end
describe "reset_page/1" do
test "restores defaults after customisation" do
{:ok, _} =
Pages.save_page("home", %{
title: "Custom",
blocks: [%{"id" => "blk_1", "type" => "hero", "settings" => %{}}]
})
assert :ok = Pages.reset_page("home")
page = Pages.get_page("home")
assert page.title == "Home page"
assert length(page.blocks) == 4
end
test "is a no-op for a page that was never saved" do
assert :ok = Pages.reset_page("cart")
end
end
describe "load_block_data/2" do
test "returns empty map when no blocks have data loaders" do
blocks = [
%{"type" => "hero", "settings" => %{}},
%{"type" => "category_nav", "settings" => %{}}
]
assert Pages.load_block_data(blocks, %{}) == %{}
end
test "loads review data when reviews_section block is present" do
blocks = [%{"type" => "reviews_section", "settings" => %{}}]
data = Pages.load_block_data(blocks, %{})
assert is_list(data.reviews)
assert data.average_rating == 5
assert data.total_count == 24
end
test "loads featured products with configurable count" do
blocks = [%{"type" => "featured_products", "settings" => %{"product_count" => 4}}]
data = Pages.load_block_data(blocks, %{mode: :preview})
assert is_list(data.products)
end
test "loads related products in preview mode" do
blocks = [%{"type" => "related_products", "settings" => %{}}]
data = Pages.load_block_data(blocks, %{mode: :preview})
assert is_list(data.related_products)
end
test "loads related products from DB with product context" do
blocks = [%{"type" => "related_products", "settings" => %{}}]
data = Pages.load_block_data(blocks, %{product: %{category: "Prints", id: "fake-id"}})
assert is_list(data.related_products)
end
test "merges data from multiple loaders" do
blocks = [
%{"type" => "featured_products", "settings" => %{}},
%{"type" => "reviews_section", "settings" => %{}}
]
data = Pages.load_block_data(blocks, %{mode: :preview})
assert Map.has_key?(data, :products)
assert Map.has_key?(data, :reviews)
end
end
describe "BlockTypes" do
test "all/0 returns all block types" do
types = BlockTypes.all()
assert is_map(types)
assert map_size(types) > 20
assert Map.has_key?(types, "hero")
assert Map.has_key?(types, "product_hero")
assert Map.has_key?(types, "search_results")
end
test "get/1 returns a block type definition" do
hero = BlockTypes.get("hero")
assert hero.name == "Hero banner"
assert hero.icon == "hero-megaphone"
assert hero.allowed_on == :all
assert is_list(hero.settings_schema)
end
test "get/1 returns nil for unknown type" do
assert BlockTypes.get("nonexistent") == nil
end
test "allowed_for/1 returns portable + page-specific blocks" do
pdp_types = BlockTypes.allowed_for("pdp")
# Portable blocks are included
assert Map.has_key?(pdp_types, "hero")
assert Map.has_key?(pdp_types, "featured_products")
# PDP-specific blocks are included
assert Map.has_key?(pdp_types, "product_hero")
assert Map.has_key?(pdp_types, "breadcrumb")
# Other page-specific blocks are excluded
refute Map.has_key?(pdp_types, "cart_items")
refute Map.has_key?(pdp_types, "contact_form")
end
test "allowed_for/1 includes only relevant page-specific blocks" do
cart_types = BlockTypes.allowed_for("cart")
assert Map.has_key?(cart_types, "cart_items")
assert Map.has_key?(cart_types, "order_summary")
refute Map.has_key?(cart_types, "product_hero")
end
test "every block type has required fields" do
for {type, def} <- BlockTypes.all() do
assert is_binary(def.name), "#{type} missing name"
assert is_binary(def.icon), "#{type} missing icon"
assert def.allowed_on == :all or is_list(def.allowed_on), "#{type} bad allowed_on"
assert is_list(def.settings_schema), "#{type} missing settings_schema"
end
end
test "settings_schema entries have required fields" do
for {type, def} <- BlockTypes.all(), setting <- def.settings_schema do
assert is_binary(setting.key), "#{type} setting missing key"
assert is_binary(setting.label), "#{type} setting missing label"
assert is_atom(setting.type), "#{type} setting missing type"
assert Map.has_key?(setting, :default), "#{type} setting #{setting.key} missing default"
end
end
end
describe "Defaults" do
test "all/0 returns 14 page definitions" do
all = Defaults.all()
assert length(all) == 14
end
test "every default page has blocks matching registered types" do
for page <- Defaults.all() do
assert is_binary(page.slug), "page missing slug"
assert is_binary(page.title), "page missing title"
assert is_list(page.blocks), "#{page.slug} blocks is not a list"
for block <- page.blocks do
assert BlockTypes.get(block["type"]),
"#{page.slug} has unknown block type: #{block["type"]}"
end
end
end
test "every default block type is allowed on its page" do
for page <- Defaults.all() do
allowed = BlockTypes.allowed_for(page.slug)
for block <- page.blocks do
assert Map.has_key?(allowed, block["type"]),
"#{block["type"]} not allowed on #{page.slug}"
end
end
end
test "block IDs are unique within each page" do
for page <- Defaults.all() do
ids = Enum.map(page.blocks, & &1["id"])
assert ids == Enum.uniq(ids), "duplicate block IDs on #{page.slug}"
end
end
test "home page has the expected blocks" do
page = Defaults.for_slug("home")
types = Enum.map(page.blocks, & &1["type"])
assert types == ["hero", "category_nav", "featured_products", "image_text"]
end
test "pdp page has the expected blocks" do
page = Defaults.for_slug("pdp")
types = Enum.map(page.blocks, & &1["type"])
assert types == [
"breadcrumb",
"product_hero",
"trust_badges",
"product_details",
"reviews_section",
"related_products"
]
end
test "contact page has the expected blocks" do
page = Defaults.for_slug("contact")
types = Enum.map(page.blocks, & &1["type"])
assert types == [
"hero",
"contact_form",
"order_tracking_card",
"info_card",
"newsletter_card",
"social_links_card"
]
end
test "generate_block_id/0 produces unique prefixed IDs" do
id1 = Defaults.generate_block_id()
id2 = Defaults.generate_block_id()
assert String.starts_with?(id1, "blk_")
assert String.starts_with?(id2, "blk_")
assert id1 != id2
end
end
describe "PageCache" do
test "get/1 returns :miss for uncached slug" do
assert :miss = PageCache.get("nonexistent_slug_#{System.unique_integer()}")
end
test "put/2 and get/1 round-trip" do
slug = "test_cache_#{System.unique_integer()}"
page_data = %{slug: slug, title: "Test", blocks: []}
assert :ok = PageCache.put(slug, page_data)
assert {:ok, ^page_data} = PageCache.get(slug)
end
test "invalidate/1 removes cached entry" do
slug = "test_invalidate_#{System.unique_integer()}"
PageCache.put(slug, %{slug: slug, title: "Test", blocks: []})
assert :ok = PageCache.invalidate(slug)
assert :miss = PageCache.get(slug)
end
end
end