348 lines
10 KiB
Elixir
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
|