diff --git a/lib/berrypod_web/page_renderer.ex b/lib/berrypod_web/page_renderer.ex
new file mode 100644
index 0000000..65f77b2
--- /dev/null
+++ b/lib/berrypod_web/page_renderer.ex
@@ -0,0 +1,755 @@
+defmodule BerrypodWeb.PageRenderer do
+ @moduledoc """
+ Generic page renderer for the page builder.
+
+ One `render_page/1` renders ALL pages — no page-specific render functions.
+ Each page is a flat list of blocks rendered in order. Block components
+ dispatch to existing shop components.
+ """
+
+ use Phoenix.Component
+ use BerrypodWeb.ShopComponents
+
+ alias Berrypod.Cart
+
+ # ── Public API ──────────────────────────────────────────────────
+
+ @doc """
+ Renders a full page from its block definition.
+
+ Expects `@page` (with `:slug` and `:blocks`) plus all the standard
+ layout assigns (theme_settings, cart_items, etc.).
+ """
+ def render_page(assigns) do
+ assigns = assign(assigns, :block_assigns, block_assigns(assigns))
+
+ ~H"""
+ <.shop_layout
+ {layout_assigns(assigns)}
+ active_page={@page.slug}
+ error_page={@page.slug == "error"}
+ >
+
+ <%= for block <- @page.blocks do %>
+ {render_block(Map.merge(@block_assigns, %{block: block, page_slug: @page.slug}))}
+ <% end %>
+
+
+ """
+ end
+
+ # ── Block dispatch ──────────────────────────────────────────────
+
+ defp render_block(%{block: %{"type" => "hero"}} = assigns) do
+ settings = assigns.block["settings"] || %{}
+ variant = hero_variant(settings["variant"])
+
+ # Page-context assigns (e.g. error_title, error_code) override block settings
+ assigns =
+ assigns
+ |> assign(:title, assigns[:error_title] || settings["title"] || "")
+ |> assign(:description, assigns[:error_description] || settings["description"] || "")
+ |> assign(:variant, variant)
+ |> assign(:background, hero_background(settings["variant"]))
+ |> assign(:pre_title, assigns[:error_code] || settings["pre_title"])
+ |> assign(:cta_text, settings["cta_text"])
+ |> assign(:cta_href, settings["cta_href"])
+ |> assign(:secondary_cta_text, settings["secondary_cta_text"])
+ |> assign(:secondary_cta_href, settings["secondary_cta_href"])
+
+ ~H"""
+ <.hero_section
+ title={@title}
+ description={@description}
+ variant={@variant}
+ background={@background}
+ pre_title={@pre_title}
+ cta_text={@cta_text}
+ cta_href={@cta_href}
+ secondary_cta_text={@secondary_cta_text}
+ secondary_cta_href={@secondary_cta_href}
+ mode={@mode}
+ />
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "category_nav"}} = assigns) do
+ ~H"""
+ <.category_nav categories={assigns[:categories] || []} mode={@mode} />
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "featured_products"}} = assigns) do
+ settings = assigns.block["settings"] || %{}
+
+ assigns =
+ assigns
+ |> assign(:section_title, settings["title"] || "Featured products")
+
+ ~H"""
+ <.featured_products_section
+ title={@section_title}
+ products={assigns[:products] || []}
+ theme_settings={@theme_settings}
+ mode={@mode}
+ />
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "image_text"}} = assigns) do
+ settings = assigns.block["settings"] || %{}
+
+ assigns =
+ assigns
+ |> assign(:section_title, settings["title"] || "")
+ |> assign(:section_description, settings["description"] || "")
+ |> assign(:image_url, settings["image_url"] || "")
+ |> assign(:link_text, settings["link_text"])
+ |> assign(:link_href, settings["link_href"])
+
+ ~H"""
+ <.image_text_section
+ title={@section_title}
+ description={@section_description}
+ image_url={@image_url}
+ link_text={@link_text}
+ link_href={@link_href}
+ mode={@mode}
+ />
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "newsletter_card"}} = assigns) do
+ ~H"<.newsletter_card />"
+ end
+
+ defp render_block(%{block: %{"type" => "social_links_card"}} = assigns) do
+ ~H"<.social_links_card />"
+ end
+
+ defp render_block(%{block: %{"type" => "info_card"}} = assigns) do
+ settings = assigns.block["settings"] || %{}
+
+ items =
+ (settings["items"] || [])
+ |> Enum.map(fn item ->
+ %{label: item["label"] || item[:label], value: item["value"] || item[:value]}
+ end)
+
+ assigns =
+ assigns
+ |> assign(:card_title, settings["title"] || "")
+ |> assign(:items, items)
+
+ ~H"""
+ <.info_card title={@card_title} items={@items} />
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "trust_badges"}} = assigns) do
+ ~H"""
+ <.trust_badges :if={@theme_settings.pdp_trust_badges} />
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "reviews_section"}} = assigns) do
+ ~H"""
+ <.reviews_section
+ :if={@theme_settings.pdp_reviews}
+ reviews={assigns[:reviews] || []}
+ average_rating={assigns[:average_rating] || 5}
+ total_count={assigns[:total_count]}
+ />
+ """
+ end
+
+ # ── PDP blocks ──────────────────────────────────────────────────
+
+ defp render_block(%{block: %{"type" => "breadcrumb"}} = assigns) do
+ ~H"""
+ <.breadcrumb
+ :if={assigns[:product]}
+ items={breadcrumb_items(assigns[:product])}
+ mode={@mode}
+ />
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "product_hero"}} = assigns) do
+ ~H"""
+ <%= if assigns[:product] do %>
+
+ <.product_gallery images={assigns[:gallery_images] || []} product_name={@product.title} />
+
+
+ <.product_info product={@product} display_price={assigns[:display_price]} />
+
+
+
+
+ <% end %>
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "product_details"}} = assigns) do
+ ~H"""
+ <.product_details :if={assigns[:product]} product={@product} />
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "related_products"}} = assigns) do
+ ~H"""
+ <.related_products_section
+ :if={@theme_settings.pdp_related_products && assigns[:related_products] != []}
+ products={assigns[:related_products] || []}
+ theme_settings={@theme_settings}
+ mode={@mode}
+ />
+ """
+ end
+
+ # ── Collection blocks ───────────────────────────────────────────
+
+ defp render_block(%{block: %{"type" => "collection_header"}} = assigns) do
+ ~H"""
+ <.collection_header
+ title={assigns[:collection_title] || "All Products"}
+ product_count={length(assigns[:products] || [])}
+ />
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "filter_bar"}} = assigns) do
+ ~H"""
+
+ <.filter_bar categories={assigns[:categories] || []} />
+
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "product_grid"}} = assigns) do
+ ~H"""
+
+ <.product_grid theme_settings={@theme_settings}>
+ <%= for product <- assigns[:products] || [] do %>
+ <.product_card
+ product={product}
+ theme_settings={@theme_settings}
+ mode={@mode}
+ variant={:default}
+ show_category={true}
+ />
+ <% end %>
+
+
+ """
+ end
+
+ # ── Cart blocks ─────────────────────────────────────────────────
+
+ defp render_block(%{block: %{"type" => "cart_items"}} = assigns) do
+ ~H"""
+ <.page_title text="Your basket" />
+ <%= if @cart_items == [] do %>
+ <.cart_empty_state mode={@mode} />
+ <% else %>
+
+ <%= for item <- @cart_items do %>
+ -
+ <.shop_card class="cart-page-card">
+ <.cart_item_row item={item} size={:default} show_quantity_controls mode={@mode} />
+
+
+ <% end %>
+
+ <% end %>
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "order_summary"}} = assigns) do
+ ~H"""
+ <%= if @cart_items != [] do %>
+ <.order_summary
+ subtotal={assigns[:cart_page_subtotal] || 0}
+ shipping_estimate={assigns[:shipping_estimate]}
+ country_code={assigns[:country_code] || "GB"}
+ available_countries={assigns[:available_countries] || []}
+ mode={@mode}
+ />
+ <% end %>
+ """
+ end
+
+ # ── Contact blocks ──────────────────────────────────────────────
+
+ defp render_block(%{block: %{"type" => "contact_form"}} = assigns) do
+ settings = assigns.block["settings"] || %{}
+ assigns = assign(assigns, :email, settings["email"])
+
+ ~H"""
+ <.contact_form email={@email} />
+ """
+ end
+
+ defp render_block(%{block: %{"type" => "order_tracking_card"}} = assigns) do
+ ~H"""
+ <.order_tracking_card tracking_state={assigns[:tracking_state] || :idle} />
+ """
+ end
+
+ # ── Content blocks ──────────────────────────────────────────────
+
+ defp render_block(%{block: %{"type" => "content_body"}} = assigns) do
+ settings = assigns.block["settings"] || %{}
+
+ assigns =
+ assigns
+ |> assign(:image_src, settings["image_src"])
+ |> assign(:image_alt, settings["image_alt"] || "")
+
+ ~H"""
+
+ <%= if @image_src && @image_src != "" do %>
+
+ <.responsive_image
+ src={@image_src}
+ source_width={1200}
+ alt={@image_alt}
+ sizes="(max-width: 800px) 100vw, 800px"
+ class="content-hero-image"
+ />
+
+ <% end %>
+
+ <.rich_text blocks={assigns[:content_blocks] || []} />
+
+ """
+ end
+
+ # ── Checkout success ────────────────────────────────────────────
+
+ defp render_block(%{block: %{"type" => "checkout_result"}} = assigns) do
+ ~H"""
+ <%= if assigns[:order] && assigns[:order].payment_status == "paid" do %>
+
+
+ <.shop_card class="checkout-card">
+ Order details
+
+ <%= for item <- assigns[:order].items do %>
+ -
+
+
{item.product_name}
+ <%= if item.variant_title do %>
+
{item.variant_title}
+ <% end %>
+
Qty: {item.quantity}
+
+
+ {Cart.format_price(item.unit_price * item.quantity)}
+
+
+ <% end %>
+
+
+
+ Total
+ {Cart.format_price(assigns[:order].total)}
+
+
+
+
+ <%= if assigns[:order].shipping_address != %{} do %>
+ <.shop_card class="checkout-card">
+ Shipping to
+
+
{assigns[:order].shipping_address["name"]}
+
{assigns[:order].shipping_address["line1"]}
+ <%= if assigns[:order].shipping_address["line2"] do %>
+
{assigns[:order].shipping_address["line2"]}
+ <% end %>
+
+ {assigns[:order].shipping_address["city"]}, {assigns[:order].shipping_address[
+ "postal_code"
+ ]}
+
+
{assigns[:order].shipping_address["country"]}
+
+
+ <% end %>
+
+
+ <.shop_link_button href="/collections/all" class="checkout-cta">
+ Continue shopping
+
+
+ <% else %>
+
+ <% end %>
+ """
+ end
+
+ # ── Orders ──────────────────────────────────────────────────────
+
+ defp render_block(%{block: %{"type" => "order_card"}} = assigns) do
+ ~H"""
+
+
+ <%= cond do %>
+ <% is_nil(assigns[:orders]) -> %>
+
+
This link has expired or is invalid.
+
+ Head back to the <.link navigate="/contact">contact page to request a new one.
+
+
+ <% assigns[:orders] == [] -> %>
+
+
No orders found for that email address.
+
+ If something doesn't look right, <.link navigate="/contact">get in touch.
+
+
+ <% true -> %>
+
+ <%= for order <- assigns[:orders] do %>
+ <.link navigate={"/orders/#{order.order_number}"} class="order-summary-card">
+
+
+
{order.order_number}
+
+ {Calendar.strftime(order.inserted_at, "%-d %B %Y")}
+
+
+
+ {format_order_status(order.fulfilment_status)}
+
+
+
+ <%= for item <- Enum.take(order.items, 2) do %>
+ -
+ {item.quantity}× {item.product_name}
+ <%= if item.variant_title && item.variant_title != "" do %>
+ · {item.variant_title}
+ <% end %>
+
+ <% end %>
+ <%= if length(order.items) > 2 do %>
+ - +{length(order.items) - 2} more
+ <% end %>
+
+
+
+ <% end %>
+
+ <% end %>
+ """
+ end
+
+ # ── Order detail ────────────────────────────────────────────────
+
+ defp render_block(%{block: %{"type" => "order_detail_card"}} = assigns) do
+ ~H"""
+ <%= if assigns[:order] do %>
+
+
+ <%= if assigns[:order].tracking_number || assigns[:order].tracking_url do %>
+ <.shop_card class="order-detail-tracking-card">
+
+
+
+
Shipment tracking
+ <%= if assigns[:order].tracking_number do %>
+
{assigns[:order].tracking_number}
+ <% end %>
+
+ <%= if assigns[:order].tracking_url do %>
+
+ Track parcel
+
+ <% end %>
+
+
+ <% end %>
+
+ <.shop_card class="checkout-card">
+ Items ordered
+
+ <%= for item <- assigns[:order].items do %>
+ <% info = (assigns[:thumbnails] || %{})[item.variant_id] %>
+ -
+ <%= if info && info.thumb do %>
+
+ <% end %>
+
+ <%= if info && info.slug do %>
+ <.link
+ navigate={"/products/#{info.slug}"}
+ class="checkout-item-name checkout-item-link"
+ >
+ {item.product_name}
+
+ <% else %>
+
{item.product_name}
+ <% end %>
+ <%= if item.variant_title && item.variant_title != "" do %>
+
{item.variant_title}
+ <% end %>
+
Qty: {item.quantity}
+
+
+ {Cart.format_price(item.unit_price * item.quantity)}
+
+
+ <% end %>
+
+
+
+ Total
+ {Cart.format_price(assigns[:order].total)}
+
+
+
+
+ <%= if assigns[:order].shipping_address != %{} do %>
+ <.shop_card class="checkout-card">
+ Shipping to
+
+
{assigns[:order].shipping_address["name"]}
+
{assigns[:order].shipping_address["line1"]}
+ <%= if assigns[:order].shipping_address["line2"] do %>
+
{assigns[:order].shipping_address["line2"]}
+ <% end %>
+
+ {assigns[:order].shipping_address["city"]}, {assigns[:order].shipping_address[
+ "postal_code"
+ ]}
+
+
{assigns[:order].shipping_address["country"]}
+
+
+ <% end %>
+
+
+ <.shop_link_button href="/collections/all">Continue shopping
+
+ <% end %>
+ """
+ end
+
+ # ── Search ──────────────────────────────────────────────────────
+
+ defp render_block(%{block: %{"type" => "search_results"}} = assigns) do
+ ~H"""
+ <.page_title text="Search" />
+
+
+
+ <%= if (assigns[:search_page_results] || []) != [] do %>
+
+ {length(assigns[:search_page_results])} {if length(assigns[:search_page_results]) == 1,
+ do: "result",
+ else: "results"} for "{assigns[:search_page_query]}"
+
+ <.product_grid theme_settings={@theme_settings}>
+ <%= for product <- assigns[:search_page_results] do %>
+ <.product_card
+ product={product}
+ theme_settings={@theme_settings}
+ mode={@mode}
+ variant={:default}
+ />
+ <% end %>
+
+ <% else %>
+ <%= if (assigns[:search_page_query] || "") != "" do %>
+
+
No products found for “{assigns[:search_page_query]}”
+ <.link navigate="/collections/all" class="collection-empty-link">Browse all products
+
+ <% end %>
+ <% end %>
+ """
+ end
+
+ # ── Fallback ────────────────────────────────────────────────────
+
+ defp render_block(assigns) do
+ ~H"""
+ <%!-- Unknown block type: {assigns.block["type"]} --%>
+ """
+ end
+
+ # ── Helpers ─────────────────────────────────────────────────────
+
+ @doc "Returns a CSS class for the page's `` element."
+ def page_main_class("contact"), do: "page-container contact-main"
+ def page_main_class("cart"), do: "page-container"
+ def page_main_class("checkout_success"), do: "page-container checkout-main"
+ def page_main_class("orders"), do: "page-container orders-main"
+ def page_main_class("order_detail"), do: "page-container order-detail-main"
+ def page_main_class("error"), do: "error-main"
+ def page_main_class("pdp"), do: "page-container"
+ def page_main_class("search"), do: "page-container"
+ def page_main_class("about"), do: "content-page"
+ def page_main_class("delivery"), do: "content-page"
+ def page_main_class("privacy"), do: "content-page"
+ def page_main_class("terms"), do: "content-page"
+ def page_main_class(_), do: nil
+
+ # Extracts the assigns that individual blocks need (everything except page metadata).
+ # Preserves __changed__ so assign/3 works inside render_block functions.
+ defp block_assigns(assigns) do
+ Map.drop(assigns, [:page, :block_assigns])
+ end
+
+ defp hero_variant("page"), do: :page
+ defp hero_variant("sunken"), do: :default
+ defp hero_variant("error"), do: :error
+ defp hero_variant(_), do: :default
+
+ defp hero_background("sunken"), do: :sunken
+ defp hero_background(_), do: :base
+
+ defp breadcrumb_items(%{category: cat, title: title}) when not is_nil(cat) do
+ slug = cat |> String.downcase() |> String.replace(" ", "-")
+
+ [
+ %{label: cat, page: "collection", href: "/collections/#{slug}"},
+ %{label: title, current: true}
+ ]
+ end
+
+ defp breadcrumb_items(%{title: title}) do
+ [%{label: title, current: true}]
+ end
+
+ defp breadcrumb_items(_), do: []
+
+ # Reuse from PageTemplates
+ def format_order_status("unfulfilled"), do: "Being prepared"
+ def format_order_status("submitted"), do: "Sent to printer"
+ def format_order_status("processing"), do: "In production"
+ def format_order_status("shipped"), do: "On its way"
+ def format_order_status("delivered"), do: "Delivered"
+ def format_order_status("failed"), do: "Issue — contact us"
+ def format_order_status("cancelled"), do: "Cancelled"
+ def format_order_status(status), do: status
+end
diff --git a/test/berrypod/pages_test.exs b/test/berrypod/pages_test.exs
index 35a78df..11a8e16 100644
--- a/test/berrypod/pages_test.exs
+++ b/test/berrypod/pages_test.exs
@@ -1,9 +1,15 @@
defmodule Berrypod.PagesTest do
- use Berrypod.DataCase, async: true
+ 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")
diff --git a/test/berrypod_web/page_renderer_test.exs b/test/berrypod_web/page_renderer_test.exs
new file mode 100644
index 0000000..6efa944
--- /dev/null
+++ b/test/berrypod_web/page_renderer_test.exs
@@ -0,0 +1,242 @@
+defmodule BerrypodWeb.PageRendererTest do
+ use BerrypodWeb.ConnCase, async: false
+
+ import Phoenix.LiveViewTest
+
+ alias Berrypod.Pages
+ alias Berrypod.Settings.ThemeSettings
+ alias BerrypodWeb.PageRenderer
+
+ # Minimal assigns that every page needs (the stuff shop_layout requires)
+ defp base_assigns do
+ %{
+ __changed__: nil,
+ theme_settings: %ThemeSettings{},
+ logo_image: nil,
+ header_image: nil,
+ mode: :shop,
+ cart_items: [],
+ cart_count: 0,
+ cart_subtotal: "£0.00",
+ cart_total: nil,
+ cart_drawer_open: false,
+ cart_status: nil,
+ is_admin: false,
+ search_query: "",
+ search_results: [],
+ search_open: false,
+ categories: [],
+ shipping_estimate: nil,
+ country_code: "GB",
+ available_countries: [],
+ flash: %{}
+ }
+ end
+
+ defp render_page(slug, extra_assigns \\ %{}) do
+ page = Pages.get_page(slug)
+
+ assigns =
+ base_assigns()
+ |> Map.merge(extra_assigns)
+ |> Map.put(:page, page)
+
+ PageRenderer.render_page(assigns)
+ |> rendered_to_string()
+ end
+
+ describe "render_page/1" do
+ test "home page renders hero and featured products" do
+ html = render_page("home", %{products: []})
+
+ assert html =~ "Original designs, printed on demand"
+ assert html =~ "Shop the collection"
+ assert html =~ "Featured products"
+ assert html =~ "Made with passion, printed with care"
+ end
+
+ test "about page renders hero and content area" do
+ html =
+ render_page("about", %{content_blocks: [%{type: :paragraph, text: "Test about text"}]})
+
+ assert html =~ "About the studio"
+ assert html =~ "content-body"
+ end
+
+ test "delivery page renders hero" do
+ html = render_page("delivery", %{content_blocks: []})
+
+ assert html =~ "Delivery information"
+ end
+
+ test "privacy page renders hero" do
+ html = render_page("privacy", %{content_blocks: []})
+
+ assert html =~ "Privacy policy"
+ end
+
+ test "terms page renders hero" do
+ html = render_page("terms", %{content_blocks: []})
+
+ assert html =~ "Terms & conditions"
+ end
+
+ test "contact page renders hero, form, and sidebar blocks" do
+ html = render_page("contact", %{tracking_state: :idle})
+
+ assert html =~ "Get in touch"
+ assert html =~ "Send a message"
+ assert html =~ "Track your order"
+ assert html =~ "Handy to know"
+ assert html =~ "Newsletter"
+ end
+
+ test "collection page renders header, filter bar, and grid" do
+ html = render_page("collection", %{products: [], collection_title: "All Products"})
+
+ assert html =~ "All Products"
+ assert html =~ "filter-bar"
+ end
+
+ test "pdp page renders breadcrumb and product hero when product provided" do
+ product = %{
+ id: "test-id",
+ title: "Test Product",
+ slug: "test-product",
+ category: "Art Prints",
+ description: "A lovely test product",
+ cheapest_price: 2500,
+ compare_at_price: nil,
+ on_sale: false,
+ in_stock: true,
+ provider_data: %{}
+ }
+
+ html =
+ render_page("pdp", %{
+ product: product,
+ gallery_images: [],
+ display_price: 2500,
+ selected_variant: nil,
+ option_types: [],
+ selected_options: %{},
+ available_options: %{},
+ option_urls: %{},
+ quantity: 1,
+ related_products: [],
+ reviews: [],
+ average_rating: 5,
+ total_count: 0
+ })
+
+ assert html =~ "Art Prints"
+ assert html =~ "Test Product"
+ assert html =~ "pdp-grid"
+ assert html =~ "Add to basket"
+ end
+
+ test "cart page renders empty state when no items" do
+ html = render_page("cart")
+
+ assert html =~ "Your basket"
+ assert html =~ "cart-empty"
+ end
+
+ test "cart page renders items when present" do
+ items = [
+ %{
+ variant_id: "v1",
+ name: "Test T-Shirt",
+ variant: "Black / M",
+ price: 2500,
+ quantity: 1,
+ image: nil,
+ product_id: "test-t-shirt",
+ in_stock: true
+ }
+ ]
+
+ html =
+ render_page("cart", %{
+ cart_items: items,
+ cart_count: 1,
+ cart_subtotal: "£25.00",
+ cart_page_subtotal: 2500
+ })
+
+ assert html =~ "Your basket"
+ assert html =~ "cart-page-list"
+ end
+
+ test "search page renders search form" do
+ html = render_page("search", %{search_page_query: "", search_page_results: []})
+
+ assert html =~ "Search"
+ assert html =~ ~s(name="q")
+ assert html =~ "Search products..."
+ end
+
+ test "checkout success renders pending state when no order" do
+ html = render_page("checkout_success", %{order: nil})
+
+ assert html =~ "Processing your payment"
+ end
+
+ test "orders page renders empty state when orders nil" do
+ html = render_page("orders", %{orders: nil, lookup_email: nil})
+
+ assert html =~ "Your orders"
+ assert html =~ "expired or is invalid"
+ end
+
+ test "orders page renders no orders message" do
+ html = render_page("orders", %{orders: [], lookup_email: "test@example.com"})
+
+ assert html =~ "test@example.com"
+ assert html =~ "No orders found"
+ end
+
+ test "order detail page renders nothing when no order" do
+ html = render_page("order_detail", %{order: nil})
+
+ # Should not crash, just render empty
+ assert html =~ "main"
+ end
+
+ test "error page renders error hero" do
+ html =
+ render_page("error", %{
+ error_code: "404",
+ error_title: "Page not found",
+ error_description: "Sorry, we couldn't find that page.",
+ products: []
+ })
+
+ assert html =~ "404"
+ assert html =~ "Page not found"
+ assert html =~ "Go to Homepage"
+ end
+ end
+
+ describe "page_main_class/1" do
+ test "returns correct classes for each page" do
+ assert PageRenderer.page_main_class("contact") == "page-container contact-main"
+ assert PageRenderer.page_main_class("cart") == "page-container"
+ assert PageRenderer.page_main_class("checkout_success") == "page-container checkout-main"
+ assert PageRenderer.page_main_class("orders") == "page-container orders-main"
+ assert PageRenderer.page_main_class("order_detail") == "page-container order-detail-main"
+ assert PageRenderer.page_main_class("error") == "error-main"
+ assert PageRenderer.page_main_class("about") == "content-page"
+ assert PageRenderer.page_main_class("home") == nil
+ end
+ end
+
+ describe "format_order_status/1" do
+ test "maps status strings to display text" do
+ assert PageRenderer.format_order_status("unfulfilled") == "Being prepared"
+ assert PageRenderer.format_order_status("shipped") == "On its way"
+ assert PageRenderer.format_order_status("delivered") == "Delivered"
+ assert PageRenderer.format_order_status("unknown") == "unknown"
+ end
+ end
+end