All checks were successful
deploy / deploy (push) Successful in 1m24s
New block types: spacer, divider, button/CTA, video embed (YouTube, Vimeo with privacy-enhanced embeds, fallback for unknown URLs). Page templates (blank, content, landing) shown when creating custom pages. Duplicate page action on admin index with slug deduplication. Fix block picker on shop edit sidebar being cut off on mobile by accounting for bottom nav and making the grid scrollable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
367 lines
10 KiB
Elixir
367 lines
10 KiB
Elixir
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 & returns"
|
|
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 of service"
|
|
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 "utility blocks" do
|
|
setup do
|
|
on_exit(fn ->
|
|
import Ecto.Query
|
|
Berrypod.Repo.delete_all(from p in Berrypod.Pages.Page, where: p.slug == "home")
|
|
Berrypod.Pages.PageCache.invalidate_all()
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
test "spacer block renders with size" do
|
|
{:ok, _} =
|
|
Pages.save_page("home", %{
|
|
title: "Home",
|
|
blocks: [%{"id" => "blk_sp", "type" => "spacer", "settings" => %{"size" => "large"}}]
|
|
})
|
|
|
|
html = render_page("home", %{products: []})
|
|
|
|
assert html =~ ~s(class="block-spacer")
|
|
assert html =~ ~s(data-size="large")
|
|
end
|
|
|
|
test "divider block renders with style" do
|
|
{:ok, _} =
|
|
Pages.save_page("home", %{
|
|
title: "Home",
|
|
blocks: [%{"id" => "blk_dv", "type" => "divider", "settings" => %{"style" => "dots"}}]
|
|
})
|
|
|
|
html = render_page("home", %{products: []})
|
|
|
|
assert html =~ ~s(class="block-divider")
|
|
assert html =~ ~s(data-style="dots")
|
|
end
|
|
|
|
test "button block renders themed link" do
|
|
{:ok, _} =
|
|
Pages.save_page("home", %{
|
|
title: "Home",
|
|
blocks: [
|
|
%{
|
|
"id" => "blk_btn",
|
|
"type" => "button",
|
|
"settings" => %{
|
|
"text" => "Shop now",
|
|
"href" => "/collections/all",
|
|
"style" => "primary",
|
|
"alignment" => "centre"
|
|
}
|
|
}
|
|
]
|
|
})
|
|
|
|
html = render_page("home", %{products: []})
|
|
|
|
assert html =~ "Shop now"
|
|
assert html =~ ~s(data-align="centre")
|
|
assert html =~ "themed-button"
|
|
end
|
|
|
|
test "video embed renders YouTube iframe" do
|
|
{:ok, _} =
|
|
Pages.save_page("home", %{
|
|
title: "Home",
|
|
blocks: [
|
|
%{
|
|
"id" => "blk_vid",
|
|
"type" => "video_embed",
|
|
"settings" => %{
|
|
"url" => "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
"caption" => "Demo video"
|
|
}
|
|
}
|
|
]
|
|
})
|
|
|
|
html = render_page("home", %{products: []})
|
|
|
|
assert html =~ "youtube-nocookie.com/embed/dQw4w9WgXcQ"
|
|
assert html =~ "Demo video"
|
|
assert html =~ "video-embed"
|
|
end
|
|
|
|
test "video embed renders Vimeo iframe" do
|
|
{:ok, _} =
|
|
Pages.save_page("home", %{
|
|
title: "Home",
|
|
blocks: [
|
|
%{
|
|
"id" => "blk_vim",
|
|
"type" => "video_embed",
|
|
"settings" => %{"url" => "https://vimeo.com/123456789"}
|
|
}
|
|
]
|
|
})
|
|
|
|
html = render_page("home", %{products: []})
|
|
|
|
assert html =~ "player.vimeo.com/video/123456789"
|
|
end
|
|
|
|
test "video embed shows fallback link for unknown URLs" do
|
|
{:ok, _} =
|
|
Pages.save_page("home", %{
|
|
title: "Home",
|
|
blocks: [
|
|
%{
|
|
"id" => "blk_unk",
|
|
"type" => "video_embed",
|
|
"settings" => %{"url" => "https://example.com/video", "caption" => "My video"}
|
|
}
|
|
]
|
|
})
|
|
|
|
html = render_page("home", %{products: []})
|
|
|
|
assert html =~ "video-embed-fallback"
|
|
assert html =~ "My video"
|
|
refute html =~ "<iframe"
|
|
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
|