rename project from SimpleshopTheme to Berrypod
All modules, configs, paths, and references updated. 836 tests pass, zero warnings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
118
test/berrypod_web/live/shop/cart_test.exs
Normal file
118
test/berrypod_web/live/shop/cart_test.exs
Normal file
@@ -0,0 +1,118 @@
|
||||
defmodule BerrypodWeb.Shop.CartTest do
|
||||
use BerrypodWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Berrypod.AccountsFixtures
|
||||
|
||||
alias Berrypod.ProductsFixtures
|
||||
|
||||
setup do
|
||||
user_fixture()
|
||||
{:ok, _} = Berrypod.Settings.set_site_live(true)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp create_cart_with_product(_context) do
|
||||
product = ProductsFixtures.complete_product_fixture(%{title: "Test Art Print"})
|
||||
variant = List.first(product.variants)
|
||||
%{product: product, variant: variant}
|
||||
end
|
||||
|
||||
defp conn_with_cart(conn, variant_id, qty \\ 1) do
|
||||
conn
|
||||
|> Phoenix.ConnTest.init_test_session(%{"cart" => [{variant_id, qty}]})
|
||||
end
|
||||
|
||||
describe "Empty cart" do
|
||||
test "renders empty cart state", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/cart")
|
||||
|
||||
assert html =~ "Your basket"
|
||||
assert html =~ "Your basket is empty"
|
||||
end
|
||||
|
||||
test "shows continue shopping link when empty", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/cart")
|
||||
|
||||
assert html =~ "Continue shopping"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Cart with items" do
|
||||
setup [:create_cart_with_product]
|
||||
|
||||
test "displays cart item name", %{conn: conn, product: product, variant: variant} do
|
||||
{:ok, _view, html} = conn |> conn_with_cart(variant.id) |> live(~p"/cart")
|
||||
|
||||
assert html =~ product.title
|
||||
end
|
||||
|
||||
test "displays order summary", %{conn: conn, variant: variant} do
|
||||
{:ok, _view, html} = conn |> conn_with_cart(variant.id) |> live(~p"/cart")
|
||||
|
||||
assert html =~ "Order summary"
|
||||
assert html =~ "Subtotal"
|
||||
end
|
||||
|
||||
test "displays formatted subtotal", %{conn: conn, variant: variant} do
|
||||
{:ok, _view, html} = conn |> conn_with_cart(variant.id) |> live(~p"/cart")
|
||||
|
||||
assert html =~ Berrypod.Cart.format_price(variant.price)
|
||||
end
|
||||
|
||||
test "displays checkout button", %{conn: conn, variant: variant} do
|
||||
{:ok, _view, html} = conn |> conn_with_cart(variant.id) |> live(~p"/cart")
|
||||
|
||||
assert html =~ "Checkout"
|
||||
end
|
||||
|
||||
test "incrementing quantity updates the display", %{
|
||||
conn: conn,
|
||||
product: product,
|
||||
variant: variant
|
||||
} do
|
||||
{:ok, view, _html} = conn |> conn_with_cart(variant.id) |> live(~p"/cart")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("#main-content button[aria-label='Increase quantity of #{product.title}']")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Quantity updated to 2"
|
||||
end
|
||||
|
||||
test "decrementing to zero removes the item", %{
|
||||
conn: conn,
|
||||
product: product,
|
||||
variant: variant
|
||||
} do
|
||||
{:ok, view, _html} = conn |> conn_with_cart(variant.id) |> live(~p"/cart")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("#main-content button[aria-label='Decrease quantity of #{product.title}']")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Your basket is empty"
|
||||
end
|
||||
|
||||
test "remove button removes the item", %{conn: conn, product: product, variant: variant} do
|
||||
{:ok, view, _html} = conn |> conn_with_cart(variant.id) |> live(~p"/cart")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("#main-content button[aria-label='Remove #{product.title} from cart']")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Your basket is empty"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Cart page title" do
|
||||
test "page title is Cart", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/cart")
|
||||
|
||||
assert html =~ "<title>Cart</title>"
|
||||
end
|
||||
end
|
||||
end
|
||||
179
test/berrypod_web/live/shop/collection_test.exs
Normal file
179
test/berrypod_web/live/shop/collection_test.exs
Normal file
@@ -0,0 +1,179 @@
|
||||
defmodule BerrypodWeb.Shop.CollectionTest do
|
||||
use BerrypodWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Berrypod.AccountsFixtures
|
||||
import Berrypod.ProductsFixtures
|
||||
|
||||
alias Berrypod.Products
|
||||
|
||||
setup do
|
||||
user_fixture()
|
||||
{:ok, _} = Berrypod.Settings.set_site_live(true)
|
||||
|
||||
pc = provider_connection_fixture()
|
||||
|
||||
print =
|
||||
product_fixture(%{
|
||||
provider_connection: pc,
|
||||
title: "Mountain Sunrise Print",
|
||||
category: "Art Prints"
|
||||
})
|
||||
|
||||
product_variant_fixture(%{product: print, title: "8x10", price: 1999})
|
||||
Products.recompute_cached_fields(print)
|
||||
|
||||
shirt =
|
||||
product_fixture(%{
|
||||
provider_connection: pc,
|
||||
title: "Forest T-Shirt",
|
||||
category: "Apparel",
|
||||
on_sale: true
|
||||
})
|
||||
|
||||
product_variant_fixture(%{
|
||||
product: shirt,
|
||||
title: "Large",
|
||||
price: 2999,
|
||||
compare_at_price: 3999
|
||||
})
|
||||
|
||||
Products.recompute_cached_fields(shirt)
|
||||
|
||||
%{print: print, shirt: shirt}
|
||||
end
|
||||
|
||||
describe "Collection page" do
|
||||
test "renders collection page for /collections/all", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/collections/all")
|
||||
|
||||
assert html =~ "All Products"
|
||||
end
|
||||
|
||||
test "renders collection page for specific category", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/collections/art-prints")
|
||||
|
||||
assert html =~ "Art Prints"
|
||||
end
|
||||
|
||||
test "displays products", %{conn: conn, print: print} do
|
||||
{:ok, _view, html} = live(conn, ~p"/collections/all")
|
||||
|
||||
assert html =~ print.title
|
||||
end
|
||||
|
||||
test "displays category filter buttons", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/collections/all")
|
||||
|
||||
assert html =~ "Art Prints"
|
||||
assert html =~ "Apparel"
|
||||
end
|
||||
|
||||
test "displays sort dropdown", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/collections/all")
|
||||
|
||||
assert html =~ "Featured"
|
||||
assert html =~ "Newest"
|
||||
assert html =~ "Price: Low to High"
|
||||
assert html =~ "Price: High to Low"
|
||||
assert html =~ "Name: A-Z"
|
||||
assert html =~ "Name: Z-A"
|
||||
end
|
||||
|
||||
test "redirects to /collections/all for unknown category", %{conn: conn} do
|
||||
{:error, {:live_redirect, %{to: to, flash: flash}}} =
|
||||
live(conn, ~p"/collections/nonexistent")
|
||||
|
||||
assert to == "/collections/all"
|
||||
assert flash["error"] == "Collection not found"
|
||||
end
|
||||
|
||||
test "filters products by category", %{conn: conn, print: print, shirt: shirt} do
|
||||
{:ok, _view, html} = live(conn, ~p"/collections/art-prints")
|
||||
|
||||
assert html =~ print.title
|
||||
refute html =~ shirt.title
|
||||
end
|
||||
end
|
||||
|
||||
describe "Sorting" do
|
||||
test "sorts by price ascending", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/collections/all")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("form[phx-change='sort_changed']")
|
||||
|> render_change(%{sort: "price_asc"})
|
||||
|
||||
assert html =~ "Price: Low to High"
|
||||
end
|
||||
|
||||
test "sorts by price descending", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/collections/all")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("form[phx-change='sort_changed']")
|
||||
|> render_change(%{sort: "price_desc"})
|
||||
|
||||
assert html =~ "Price: High to Low"
|
||||
end
|
||||
|
||||
test "sorts by name ascending", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/collections/all")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("form[phx-change='sort_changed']")
|
||||
|> render_change(%{sort: "name_asc"})
|
||||
|
||||
assert html =~ "Name: A-Z"
|
||||
end
|
||||
|
||||
test "sorts by name descending", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/collections/all")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("form[phx-change='sort_changed']")
|
||||
|> render_change(%{sort: "name_desc"})
|
||||
|
||||
assert html =~ "Name: Z-A"
|
||||
end
|
||||
|
||||
test "sort parameter is preserved in URL", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/collections/all?sort=price_asc")
|
||||
|
||||
html = render(view)
|
||||
|
||||
assert html =~ "selected"
|
||||
end
|
||||
|
||||
test "default sort is featured", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/collections/all")
|
||||
|
||||
assert html =~ ~r/<option[^>]*value="featured"[^>]*selected/
|
||||
end
|
||||
end
|
||||
|
||||
describe "Navigation" do
|
||||
test "category links preserve sort order", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/collections/all?sort=price_desc")
|
||||
|
||||
assert html =~ "/collections/art-prints?sort=price_desc"
|
||||
end
|
||||
|
||||
test "All button link preserves sort order", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/collections/art-prints?sort=name_asc")
|
||||
|
||||
assert html =~ "/collections/all?sort=name_asc"
|
||||
end
|
||||
|
||||
test "featured sort does not include query param in links", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/collections/all")
|
||||
|
||||
assert html =~ ~s(href="/collections/art-prints")
|
||||
refute html =~ "/collections/art-prints?sort=featured"
|
||||
end
|
||||
end
|
||||
end
|
||||
77
test/berrypod_web/live/shop/coming_soon_test.exs
Normal file
77
test/berrypod_web/live/shop/coming_soon_test.exs
Normal file
@@ -0,0 +1,77 @@
|
||||
defmodule BerrypodWeb.Shop.ComingSoonTest do
|
||||
use BerrypodWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Berrypod.AccountsFixtures
|
||||
|
||||
alias Berrypod.Settings
|
||||
|
||||
describe "coming soon page" do
|
||||
test "renders when site is not live and admin exists", %{conn: conn} do
|
||||
user_fixture()
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/coming-soon")
|
||||
|
||||
assert html =~ "Coming soon"
|
||||
assert html =~ "getting things ready"
|
||||
end
|
||||
|
||||
test "displays the shop name", %{conn: conn} do
|
||||
{:ok, _} = Settings.update_theme_settings(%{site_name: "My Test Shop"})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/coming-soon")
|
||||
|
||||
assert html =~ "My Test Shop"
|
||||
end
|
||||
end
|
||||
|
||||
describe "site live gate" do
|
||||
test "redirects unauthenticated visitors to coming soon when not live", %{conn: conn} do
|
||||
# Create admin so the gate activates (fresh installs bypass)
|
||||
user_fixture()
|
||||
|
||||
assert {:error, {:redirect, %{to: "/coming-soon"}}} = live(conn, ~p"/")
|
||||
end
|
||||
|
||||
test "allows authenticated admin through when not live", %{conn: conn} do
|
||||
user = user_fixture()
|
||||
conn = log_in_user(conn, user)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ "Shop the collection"
|
||||
end
|
||||
|
||||
test "allows everyone through when site is live", %{conn: conn} do
|
||||
user_fixture()
|
||||
{:ok, _} = Settings.set_site_live(true)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ "Shop the collection"
|
||||
end
|
||||
|
||||
test "redirects to registration on fresh install (no admin)", %{conn: conn} do
|
||||
# No admin created — redirect to registration
|
||||
assert {:error, {:redirect, %{to: "/users/register"}}} = live(conn, ~p"/")
|
||||
end
|
||||
|
||||
test "redirects when session token is stale (user deleted)", %{conn: conn} do
|
||||
user = user_fixture()
|
||||
conn = log_in_user(conn, user)
|
||||
|
||||
# Delete the user — session cookie is now stale
|
||||
Berrypod.Repo.delete!(user)
|
||||
|
||||
assert {:error, {:redirect, %{to: "/users/register"}}} = live(conn, ~p"/")
|
||||
end
|
||||
|
||||
test "gates all public shop routes", %{conn: conn} do
|
||||
user_fixture()
|
||||
|
||||
assert {:error, {:redirect, %{to: "/coming-soon"}}} = live(conn, ~p"/about")
|
||||
assert {:error, {:redirect, %{to: "/coming-soon"}}} = live(conn, ~p"/collections/all")
|
||||
assert {:error, {:redirect, %{to: "/coming-soon"}}} = live(conn, ~p"/cart")
|
||||
end
|
||||
end
|
||||
end
|
||||
96
test/berrypod_web/live/shop/content_test.exs
Normal file
96
test/berrypod_web/live/shop/content_test.exs
Normal file
@@ -0,0 +1,96 @@
|
||||
defmodule BerrypodWeb.Shop.ContentTest do
|
||||
use BerrypodWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Berrypod.AccountsFixtures
|
||||
|
||||
setup do
|
||||
user_fixture()
|
||||
{:ok, _} = Berrypod.Settings.set_site_live(true)
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "About page" do
|
||||
test "renders about page", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/about")
|
||||
|
||||
assert html =~ "About the studio"
|
||||
assert html =~ "sample about page"
|
||||
end
|
||||
|
||||
test "displays about image", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/about")
|
||||
|
||||
assert html =~ "night-sky-blanket"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Delivery page" do
|
||||
test "renders delivery page", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/delivery")
|
||||
|
||||
assert html =~ "Delivery & returns"
|
||||
assert html =~ "shipping and returns"
|
||||
end
|
||||
|
||||
test "displays delivery content", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/delivery")
|
||||
|
||||
assert html =~ "Shipping"
|
||||
assert html =~ "Returns & exchanges"
|
||||
assert html =~ "Cancellations"
|
||||
end
|
||||
|
||||
test "displays list items", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/delivery")
|
||||
|
||||
assert html =~ "United Kingdom"
|
||||
assert html =~ "5–8 business days"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Privacy page" do
|
||||
test "renders privacy page", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/privacy")
|
||||
|
||||
assert html =~ "Privacy policy"
|
||||
assert html =~ "personal information"
|
||||
end
|
||||
|
||||
test "displays privacy content", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/privacy")
|
||||
|
||||
assert html =~ "What we collect"
|
||||
assert html =~ "Cookies"
|
||||
assert html =~ "Your rights"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Terms page" do
|
||||
test "renders terms page", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/terms")
|
||||
|
||||
assert html =~ "Terms of service"
|
||||
assert html =~ "The legal bits"
|
||||
end
|
||||
|
||||
test "displays terms content", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/terms")
|
||||
|
||||
assert html =~ "Products"
|
||||
assert html =~ "Orders & payment"
|
||||
assert html =~ "Intellectual property"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Footer links" do
|
||||
test "footer contains policy page links", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/about")
|
||||
|
||||
assert html =~ ~s(href="/delivery")
|
||||
assert html =~ ~s(href="/privacy")
|
||||
assert html =~ ~s(href="/terms")
|
||||
assert html =~ ~s(href="/contact")
|
||||
end
|
||||
end
|
||||
end
|
||||
101
test/berrypod_web/live/shop/home_test.exs
Normal file
101
test/berrypod_web/live/shop/home_test.exs
Normal file
@@ -0,0 +1,101 @@
|
||||
defmodule BerrypodWeb.Shop.HomeTest do
|
||||
use BerrypodWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Berrypod.AccountsFixtures
|
||||
import Berrypod.ProductsFixtures
|
||||
|
||||
setup do
|
||||
user_fixture()
|
||||
{:ok, _} = Berrypod.Settings.set_site_live(true)
|
||||
|
||||
conn = provider_connection_fixture()
|
||||
|
||||
product =
|
||||
product_fixture(%{
|
||||
provider_connection: conn,
|
||||
title: "Mountain Sunrise Print",
|
||||
category: "Art Prints"
|
||||
})
|
||||
|
||||
product_variant_fixture(%{product: product, title: "8x10", price: 1999})
|
||||
|
||||
# Recompute so cheapest_price is set
|
||||
Berrypod.Products.recompute_cached_fields(product)
|
||||
|
||||
%{product: product}
|
||||
end
|
||||
|
||||
describe "Home page" do
|
||||
test "renders the home page", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ "Original designs, printed on demand"
|
||||
end
|
||||
|
||||
test "renders hero section with CTA", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ "Shop the collection"
|
||||
end
|
||||
|
||||
test "renders category navigation with real categories", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ "Art Prints"
|
||||
end
|
||||
|
||||
test "renders featured products section", %{conn: conn, product: product} do
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ "Featured products"
|
||||
assert html =~ product.title
|
||||
end
|
||||
|
||||
test "renders image and text section", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ "Made with passion, printed with care"
|
||||
assert html =~ "Learn more about the studio"
|
||||
end
|
||||
|
||||
test "renders header with shop name", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ "Store Name"
|
||||
end
|
||||
|
||||
test "renders footer with links", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ ~s(href="/about")
|
||||
assert html =~ ~s(href="/contact")
|
||||
end
|
||||
end
|
||||
|
||||
describe "Navigation links" do
|
||||
test "category links point to collections", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ ~s(href="/collections/)
|
||||
end
|
||||
|
||||
test "product links point to product pages", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ ~s(href="/products/)
|
||||
end
|
||||
|
||||
test "hero CTA links to collections", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ ~s(href="/collections/all")
|
||||
end
|
||||
|
||||
test "about link in image section", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ ~s(href="/about")
|
||||
end
|
||||
end
|
||||
end
|
||||
412
test/berrypod_web/live/shop/product_show_test.exs
Normal file
412
test/berrypod_web/live/shop/product_show_test.exs
Normal file
@@ -0,0 +1,412 @@
|
||||
defmodule BerrypodWeb.Shop.ProductShowTest do
|
||||
use BerrypodWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Berrypod.AccountsFixtures
|
||||
import Berrypod.ProductsFixtures
|
||||
|
||||
alias Berrypod.Products
|
||||
|
||||
setup do
|
||||
user_fixture()
|
||||
{:ok, _} = Berrypod.Settings.set_site_live(true)
|
||||
|
||||
pc = provider_connection_fixture()
|
||||
|
||||
# Art print with size options and 2 images (for gallery tests)
|
||||
print =
|
||||
product_fixture(%{
|
||||
provider_connection: pc,
|
||||
title: "Mountain Sunrise Print",
|
||||
category: "Art Prints",
|
||||
description: "A beautiful mountain sunrise art print",
|
||||
provider_data: %{
|
||||
"options" => [
|
||||
%{
|
||||
"name" => "Size",
|
||||
"type" => "size",
|
||||
"values" => [
|
||||
%{"title" => "8x10"},
|
||||
%{"title" => "12x18"},
|
||||
%{"title" => "18x24"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
product_variant_fixture(%{
|
||||
product: print,
|
||||
title: "8x10",
|
||||
price: 1999,
|
||||
options: %{"Size" => "8x10"}
|
||||
})
|
||||
|
||||
product_variant_fixture(%{
|
||||
product: print,
|
||||
title: "12x18",
|
||||
price: 2400,
|
||||
options: %{"Size" => "12x18"}
|
||||
})
|
||||
|
||||
product_variant_fixture(%{
|
||||
product: print,
|
||||
title: "18x24",
|
||||
price: 3200,
|
||||
options: %{"Size" => "18x24"}
|
||||
})
|
||||
|
||||
product_image_fixture(%{
|
||||
product: print,
|
||||
position: 0,
|
||||
src: "https://example.com/print-1.jpg"
|
||||
})
|
||||
|
||||
product_image_fixture(%{
|
||||
product: print,
|
||||
position: 1,
|
||||
src: "https://example.com/print-2.jpg"
|
||||
})
|
||||
|
||||
Products.recompute_cached_fields(print)
|
||||
|
||||
# T-shirt with colour + size options (some sizes unavailable in white)
|
||||
shirt =
|
||||
product_fixture(%{
|
||||
provider_connection: pc,
|
||||
title: "Forest T-Shirt",
|
||||
category: "Apparel",
|
||||
description: "A forest themed t-shirt",
|
||||
provider_data: %{
|
||||
"options" => [
|
||||
%{
|
||||
"name" => "Color",
|
||||
"type" => "color",
|
||||
"values" => [
|
||||
%{"title" => "Black", "colors" => ["#000000"]},
|
||||
%{"title" => "White", "colors" => ["#FFFFFF"]}
|
||||
]
|
||||
},
|
||||
%{
|
||||
"name" => "Size",
|
||||
"type" => "size",
|
||||
"values" => [
|
||||
%{"title" => "M"},
|
||||
%{"title" => "L"},
|
||||
%{"title" => "XL"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
for color <- ["Black", "White"], size <- ["M", "L"] do
|
||||
product_variant_fixture(%{
|
||||
product: shirt,
|
||||
title: "#{color} / #{size}",
|
||||
price: 2999,
|
||||
options: %{"Color" => color, "Size" => size}
|
||||
})
|
||||
end
|
||||
|
||||
# Black / XL available
|
||||
product_variant_fixture(%{
|
||||
product: shirt,
|
||||
title: "Black / XL",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Black", "Size" => "XL"}
|
||||
})
|
||||
|
||||
# White / XL unavailable
|
||||
product_variant_fixture(%{
|
||||
product: shirt,
|
||||
title: "White / XL",
|
||||
price: 2999,
|
||||
options: %{"Color" => "White", "Size" => "XL"},
|
||||
is_available: false
|
||||
})
|
||||
|
||||
product_image_fixture(%{
|
||||
product: shirt,
|
||||
position: 0,
|
||||
src: "https://example.com/shirt-1.jpg"
|
||||
})
|
||||
|
||||
Products.recompute_cached_fields(shirt)
|
||||
|
||||
# Another art print for related products
|
||||
related =
|
||||
product_fixture(%{
|
||||
provider_connection: pc,
|
||||
title: "Ocean Waves Print",
|
||||
category: "Art Prints"
|
||||
})
|
||||
|
||||
product_variant_fixture(%{
|
||||
product: related,
|
||||
title: "12x18",
|
||||
price: 2400,
|
||||
options: %{"Size" => "12x18"}
|
||||
})
|
||||
|
||||
Products.recompute_cached_fields(related)
|
||||
|
||||
%{print: print, shirt: shirt, related: related}
|
||||
end
|
||||
|
||||
describe "Product detail page" do
|
||||
test "renders product page with product name", %{conn: conn, print: print} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert html =~ print.title
|
||||
end
|
||||
|
||||
test "renders product description", %{conn: conn, print: print} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert html =~ print.description
|
||||
end
|
||||
|
||||
test "renders product price", %{conn: conn, print: print} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
# Cheapest variant is 8x10 at £19.99
|
||||
assert html =~ "£19.99"
|
||||
end
|
||||
|
||||
test "renders breadcrumb with category link", %{conn: conn, print: print} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert html =~ "Art Prints"
|
||||
assert html =~ "/collections/"
|
||||
end
|
||||
|
||||
test "renders add to cart button", %{conn: conn, print: print} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert html =~ "Add to basket"
|
||||
end
|
||||
|
||||
test "renders related products section", %{conn: conn, print: print, related: related} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert html =~ related.title
|
||||
end
|
||||
end
|
||||
|
||||
describe "Variant selection" do
|
||||
test "renders variant selectors for product with size options", %{conn: conn, print: print} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert html =~ "Size"
|
||||
assert html =~ "8x10"
|
||||
assert html =~ "12x18"
|
||||
assert html =~ "18x24"
|
||||
end
|
||||
|
||||
test "renders colour and size selectors for apparel", %{conn: conn, shirt: shirt} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{shirt.slug}")
|
||||
|
||||
assert html =~ "Color"
|
||||
assert html =~ "Size"
|
||||
end
|
||||
|
||||
test "selecting a size updates the price", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button[phx-value-selected='18x24']")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "£32.00"
|
||||
end
|
||||
|
||||
test "selecting a colour auto-adjusts size if needed", %{conn: conn, shirt: shirt} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{shirt.slug}")
|
||||
|
||||
# Select White — M and L are available, XL is not
|
||||
html =
|
||||
view
|
||||
|> element("button[aria-label='Select White']")
|
||||
|> render_click()
|
||||
|
||||
# White is selected, size M should still be selected (valid combo)
|
||||
assert html =~ ~s(aria-pressed="true")
|
||||
end
|
||||
|
||||
test "shows variant for single-variant product", %{conn: conn, related: related} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{related.slug}")
|
||||
|
||||
# Ocean Waves Print has no provider_data options, so shows "One size"
|
||||
assert html =~ "One size"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Quantity selector" do
|
||||
test "renders quantity selector with initial value of 1", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert has_element?(view, "button[phx-click='decrement_quantity']")
|
||||
assert has_element?(view, "button[phx-click='increment_quantity']")
|
||||
end
|
||||
|
||||
test "decrement button is disabled at quantity 1", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert has_element?(view, "button[phx-click='decrement_quantity'][disabled]")
|
||||
end
|
||||
|
||||
test "increment increases quantity", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button[phx-click='increment_quantity']")
|
||||
|> render_click()
|
||||
|
||||
# Quantity now 2, decrement no longer disabled
|
||||
refute html =~ ~s(phx-click="decrement_quantity" disabled)
|
||||
end
|
||||
|
||||
test "decrement decreases quantity", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
# Increment twice to get to 3
|
||||
view |> element("button[phx-click='increment_quantity']") |> render_click()
|
||||
view |> element("button[phx-click='increment_quantity']") |> render_click()
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button[phx-click='decrement_quantity']")
|
||||
|> render_click()
|
||||
|
||||
# Should be back to 2 — decrement still enabled
|
||||
refute html =~ ~s(phx-click="decrement_quantity" disabled)
|
||||
end
|
||||
|
||||
test "quantity resets to 1 after adding to cart", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
# Increment to 3
|
||||
view |> element("button[phx-click='increment_quantity']") |> render_click()
|
||||
view |> element("button[phx-click='increment_quantity']") |> render_click()
|
||||
|
||||
# Add to cart
|
||||
html =
|
||||
view
|
||||
|> element("button", "Add to basket")
|
||||
|> render_click()
|
||||
|
||||
# Decrement should be disabled again (quantity reset to 1)
|
||||
assert html =~ ~s(phx-click="decrement_quantity")
|
||||
assert html =~ "disabled"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Add to cart" do
|
||||
test "add to cart opens the cart drawer", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button", "Add to basket")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "added to cart"
|
||||
end
|
||||
|
||||
test "add to cart shows product in cart drawer", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button", "Add to basket")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Mountain Sunrise Print"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Product gallery" do
|
||||
test "renders carousel with hook and accessibility attrs", %{conn: conn, print: print} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert html =~ ~s(phx-hook="ProductImageScroll")
|
||||
assert html =~ ~s(role="region")
|
||||
assert html =~ ~s(aria-label="Product images")
|
||||
end
|
||||
|
||||
test "renders all gallery images with alt text", %{conn: conn, print: print} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert html =~ "#{print.title} — image 1 of"
|
||||
assert html =~ "#{print.title} — image 2 of"
|
||||
end
|
||||
|
||||
test "renders dot indicators for multi-image gallery", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert has_element?(view, ".product-image-dots")
|
||||
assert has_element?(view, ".product-image-dot")
|
||||
end
|
||||
|
||||
test "renders thumbnail grid for multi-image gallery", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert has_element?(view, ".pdp-gallery-thumbs")
|
||||
assert has_element?(view, ".pdp-thumbnail")
|
||||
end
|
||||
|
||||
test "renders prev/next navigation arrows", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert has_element?(view, "button.pdp-nav-prev")
|
||||
assert has_element?(view, "button.pdp-nav-next")
|
||||
assert has_element?(view, ~s(button[aria-label="Previous image"]))
|
||||
assert has_element?(view, ~s(button[aria-label="Next image"]))
|
||||
end
|
||||
|
||||
test "renders lightbox dialog", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert has_element?(view, "dialog#pdp-lightbox")
|
||||
end
|
||||
|
||||
test "renders lightbox click target for desktop", %{conn: conn, print: print} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert has_element?(view, ".pdp-lightbox-click")
|
||||
end
|
||||
|
||||
test "thumbnails have correct aria-labels", %{conn: conn, print: print} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
|
||||
|
||||
assert html =~ "View image 1 of"
|
||||
assert html =~ "View image 2 of"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Product gallery edge cases" do
|
||||
test "single image renders without carousel or dots", %{conn: conn, shirt: shirt} do
|
||||
{:ok, view, _html} = live(conn, ~p"/products/#{shirt.slug}")
|
||||
|
||||
# Shirt has only 1 image — should render single view, not carousel
|
||||
assert has_element?(view, ".pdp-gallery-single")
|
||||
refute has_element?(view, ".product-image-dots")
|
||||
end
|
||||
end
|
||||
|
||||
describe "Navigation" do
|
||||
test "product links navigate to correct product page", %{conn: conn, shirt: shirt} do
|
||||
{:ok, _view, html} = live(conn, ~p"/products/#{shirt.slug}")
|
||||
|
||||
assert html =~ shirt.title
|
||||
end
|
||||
|
||||
test "unknown slug redirects to collections", %{conn: conn} do
|
||||
assert {:error, {:live_redirect, %{to: "/collections/all"}}} =
|
||||
live(conn, ~p"/products/nonexistent")
|
||||
end
|
||||
end
|
||||
end
|
||||
131
test/berrypod_web/live/shop/search_integration_test.exs
Normal file
131
test/berrypod_web/live/shop/search_integration_test.exs
Normal file
@@ -0,0 +1,131 @@
|
||||
defmodule BerrypodWeb.Shop.SearchIntegrationTest do
|
||||
use BerrypodWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Berrypod.AccountsFixtures
|
||||
import Berrypod.ProductsFixtures
|
||||
|
||||
alias Berrypod.Search
|
||||
|
||||
setup do
|
||||
user_fixture()
|
||||
{:ok, _} = Berrypod.Settings.set_site_live(true)
|
||||
|
||||
pc = provider_connection_fixture()
|
||||
|
||||
mountain =
|
||||
product_fixture(%{
|
||||
provider_connection: pc,
|
||||
title: "Mountain Sunrise Print",
|
||||
description: "A landscape scene at dawn",
|
||||
category: "Art Prints"
|
||||
})
|
||||
|
||||
product_variant_fixture(%{product: mountain, title: "8x10", price: 1999})
|
||||
Berrypod.Products.recompute_cached_fields(mountain)
|
||||
|
||||
ocean =
|
||||
product_fixture(%{
|
||||
provider_connection: pc,
|
||||
title: "Ocean Waves Notebook",
|
||||
description: "Spiral-bound notebook with ocean art",
|
||||
category: "Stationery"
|
||||
})
|
||||
|
||||
product_variant_fixture(%{product: ocean, title: "A5", price: 1299})
|
||||
Berrypod.Products.recompute_cached_fields(ocean)
|
||||
|
||||
Search.rebuild_index()
|
||||
|
||||
%{mountain: mountain, ocean: ocean}
|
||||
end
|
||||
|
||||
describe "search event" do
|
||||
test "returns matching products", %{conn: conn, mountain: mountain} do
|
||||
{:ok, view, _html} = live(conn, ~p"/")
|
||||
|
||||
html = render_hook(view, "search", %{"value" => "mountain"})
|
||||
|
||||
assert html =~ mountain.title
|
||||
assert html =~ ~p"/products/#{mountain.slug}"
|
||||
end
|
||||
|
||||
test "shows no results message for unmatched query", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/")
|
||||
|
||||
html = render_hook(view, "search", %{"value" => "xyznonexistent"})
|
||||
|
||||
assert html =~ "No products found"
|
||||
end
|
||||
|
||||
test "ignores short queries", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/")
|
||||
|
||||
html = render_hook(view, "search", %{"value" => "a"})
|
||||
|
||||
refute html =~ "No products found"
|
||||
refute html =~ ~s(role="option")
|
||||
end
|
||||
end
|
||||
|
||||
describe "clear_search event" do
|
||||
test "resets search state", %{conn: conn, mountain: mountain} do
|
||||
{:ok, view, _html} = live(conn, ~p"/")
|
||||
|
||||
# First search to get results
|
||||
html = render_hook(view, "search", %{"value" => "mountain"})
|
||||
assert html =~ mountain.title
|
||||
|
||||
# Clear search
|
||||
html = render_hook(view, "clear_search", %{})
|
||||
|
||||
refute html =~ ~s(No products found)
|
||||
# Results list should be gone
|
||||
refute html =~ ~s(role="option")
|
||||
end
|
||||
end
|
||||
|
||||
describe "search results rendering" do
|
||||
test "result links use navigate for LiveView navigation", %{conn: conn, mountain: mountain} do
|
||||
{:ok, view, _html} = live(conn, ~p"/")
|
||||
|
||||
html = render_hook(view, "search", %{"value" => "mountain"})
|
||||
|
||||
assert html =~ ~s(href="/products/#{mountain.slug}")
|
||||
assert html =~ ~s(data-phx-link="redirect")
|
||||
end
|
||||
|
||||
test "results have ARIA listbox and option roles", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/")
|
||||
|
||||
html = render_hook(view, "search", %{"value" => "mountain"})
|
||||
|
||||
assert html =~ ~s(role="listbox")
|
||||
assert html =~ ~s(role="option")
|
||||
end
|
||||
|
||||
test "search input has combobox ARIA attributes", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
|
||||
assert html =~ ~s(role="combobox")
|
||||
assert html =~ ~s(aria-autocomplete="list")
|
||||
assert html =~ ~s(aria-controls="search-results-list")
|
||||
end
|
||||
|
||||
test "multiple results render in order", %{conn: conn, mountain: mountain, ocean: ocean} do
|
||||
{:ok, view, _html} = live(conn, ~p"/")
|
||||
|
||||
html = render_hook(view, "search", %{"value" => "print notebook"})
|
||||
|
||||
assert html =~ mountain.title || html =~ ocean.title
|
||||
end
|
||||
|
||||
test "shows category and price in results", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/")
|
||||
|
||||
html = render_hook(view, "search", %{"value" => "mountain"})
|
||||
|
||||
assert html =~ "Art Prints"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user