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:
jamey
2026-02-18 21:23:15 +00:00
parent c65e777832
commit 9528700862
300 changed files with 23932 additions and 1349 deletions

View File

@@ -0,0 +1,117 @@
defmodule BerrypodWeb.Admin.DashboardTest do
use BerrypodWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
import Berrypod.OrdersFixtures
import Berrypod.ProductsFixtures
setup do
user = user_fixture()
%{user: user}
end
describe "unauthenticated" do
test "redirects to login", %{conn: conn} do
{:error, redirect} = live(conn, ~p"/admin")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "setup stepper" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "shows stepper with printify form when nothing connected", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin")
assert html =~ "Setup steps"
assert html =~ "Connect to Printify"
assert html =~ "Printify API token"
assert html =~ "Connect Stripe"
assert html =~ "Go live"
end
test "shows stripe form when printify is done", %{conn: conn} do
conn_fixture = provider_connection_fixture(%{provider_type: "printify"})
_product = product_fixture(%{provider_connection: conn_fixture})
{:ok, view, _html} = live(conn, ~p"/admin")
# Printify step should be completed
assert has_element?(view, "li:first-child [class*='bg-green-500']")
# Stripe step should be active with form
assert has_element?(view, "label", "Secret key")
end
test "shows go live button when all services connected", %{conn: conn} do
conn_fixture = provider_connection_fixture(%{provider_type: "printify"})
_product = product_fixture(%{provider_connection: conn_fixture})
{:ok, _} = Berrypod.Settings.put_secret("stripe_api_key", "sk_test_123")
{:ok, view, _html} = live(conn, ~p"/admin")
assert has_element?(view, "button", "Go live")
end
test "go live shows celebration", %{conn: conn} do
conn_fixture = provider_connection_fixture(%{provider_type: "printify"})
_product = product_fixture(%{provider_connection: conn_fixture})
{:ok, _} = Berrypod.Settings.put_secret("stripe_api_key", "sk_test_123")
{:ok, view, _html} = live(conn, ~p"/admin")
html = view |> element("button", "Go live") |> render_click()
assert html =~ "Your shop is live!"
assert html =~ "View your shop"
assert html =~ "Customise theme"
end
test "hides stepper when shop is live", %{conn: conn} do
{:ok, _} = Berrypod.Settings.set_site_live(true)
{:ok, _view, html} = live(conn, ~p"/admin")
refute html =~ "Setup steps"
refute html =~ "Printify API token"
end
test "completed steps show summary and are collapsible", %{conn: conn} do
conn_fixture = provider_connection_fixture(%{provider_type: "printify"})
_product = product_fixture(%{provider_connection: conn_fixture})
{:ok, _view, html} = live(conn, ~p"/admin")
assert html =~ "products synced"
end
end
describe "stats" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "shows stats cards", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin")
assert has_element?(view, "a[href='/admin/orders']", "Orders")
assert has_element?(view, "a[href='/admin/products']", "Products")
end
test "shows zero state for orders", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin")
assert html =~ "No orders yet"
end
test "shows recent orders when they exist", %{conn: conn} do
order = order_fixture(%{payment_status: "paid"})
{:ok, _view, html} = live(conn, ~p"/admin")
assert html =~ order.order_number
assert html =~ "Recent orders"
end
end
end

View File

@@ -0,0 +1,98 @@
defmodule BerrypodWeb.Admin.LayoutTest do
use BerrypodWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
setup do
user = user_fixture()
%{user: user}
end
describe "admin sidebar" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "renders sidebar nav links on admin pages", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/orders")
assert has_element?(view, ~s(a[href="/admin"]), "Dashboard")
assert has_element?(view, ~s(a[href="/admin/orders"]), "Orders")
assert has_element?(view, ~s(a[href="/admin/theme"]), "Theme")
assert has_element?(view, ~s(a[href="/admin/settings"]), "Settings")
end
test "highlights active nav link for current page", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/orders")
assert has_element?(view, ~s(a.active[href="/admin/orders"]))
refute has_element?(view, ~s(a.active[href="/admin/settings"]))
end
test "highlights dashboard on dashboard page", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin")
assert has_element?(view, ~s(a.active[href="/admin"]))
refute has_element?(view, ~s(a.active[href="/admin/orders"]))
end
test "highlights correct link on different pages", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
assert has_element?(view, ~s(a.active[href="/admin/settings"]))
refute has_element?(view, ~s(a.active[href="/admin/orders"]))
end
test "shows user email in sidebar", %{conn: conn, user: user} do
{:ok, _view, html} = live(conn, ~p"/admin/orders")
assert html =~ user.email
end
test "shows view shop and log out links", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/orders")
assert has_element?(view, ~s(a[href="/"]), "View shop")
assert has_element?(view, ~s(a[href="/users/log-out"]), "Log out")
end
end
describe "theme editor layout" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "does not render sidebar", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/theme")
refute html =~ "admin-drawer"
end
test "shows back link to admin", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/theme")
assert has_element?(view, ~s(a[href="/admin"]), "Admin")
end
end
describe "admin bar on shop pages" do
setup do
{:ok, _} = Berrypod.Settings.set_site_live(true)
:ok
end
test "shows admin link when logged in", %{conn: conn, user: user} do
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/")
assert html =~ ~s(href="/admin")
end
test "does not show admin link when logged out", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/")
refute html =~ ~s(href="/admin")
end
end
end

View File

@@ -0,0 +1,190 @@
defmodule BerrypodWeb.Admin.OrdersTest do
use BerrypodWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
import Berrypod.OrdersFixtures
setup do
user = user_fixture()
%{user: user}
end
describe "unauthenticated" do
test "redirects to login", %{conn: conn} do
{:error, redirect} = live(conn, ~p"/admin/orders")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "order list" do
setup %{conn: conn, user: user} do
conn = log_in_user(conn, user)
%{conn: conn}
end
test "renders empty state when no orders", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/orders")
assert html =~ "No orders yet"
assert html =~ "Orders"
end
test "renders orders table", %{conn: conn} do
order = order_fixture(payment_status: "paid", customer_email: "test@shop.com")
{:ok, _view, html} = live(conn, ~p"/admin/orders")
assert html =~ order.order_number
assert html =~ "test@shop.com"
assert html =~ "paid"
end
test "filters by status", %{conn: conn} do
paid = order_fixture(payment_status: "paid")
_pending = order_fixture()
{:ok, view, _html} = live(conn, ~p"/admin/orders")
html = render_click(view, "filter", %{"status" => "paid"})
assert html =~ paid.order_number
end
test "navigates to order detail", %{conn: conn} do
order = order_fixture(payment_status: "paid")
{:ok, _view, html} = live(conn, ~p"/admin/orders")
assert html =~ ~p"/admin/orders/#{order}"
end
end
describe "order detail" do
setup %{conn: conn, user: user} do
conn = log_in_user(conn, user)
%{conn: conn}
end
test "renders order details", %{conn: conn} do
order = order_fixture(payment_status: "paid", customer_email: "buyer@example.com")
{:ok, _view, html} = live(conn, ~p"/admin/orders/#{order}")
assert html =~ order.order_number
assert html =~ "buyer@example.com"
assert html =~ "paid"
assert html =~ "Order details"
end
test "shows line items", %{conn: conn} do
order = order_fixture(product_name: "Cool T-shirt", variant_title: "Blue / XL")
{:ok, _view, html} = live(conn, ~p"/admin/orders/#{order}")
assert html =~ "Cool T-shirt"
assert html =~ "Blue / XL"
end
test "shows shipping address", %{conn: conn} do
order = order_fixture(payment_status: "paid")
{:ok, updated_order} =
Berrypod.Orders.update_order(order, %{
shipping_address: %{
"name" => "Jane Doe",
"line1" => "42 Test Street",
"city" => "London",
"postal_code" => "SW1A 1AA",
"country" => "GB"
}
})
{:ok, _view, html} = live(conn, ~p"/admin/orders/#{updated_order}")
assert html =~ "Jane Doe"
assert html =~ "42 Test Street"
assert html =~ "London"
assert html =~ "SW1A 1AA"
end
test "redirects when order not found", %{conn: conn} do
fake_id = Ecto.UUID.generate()
{:error, {:live_redirect, %{to: "/admin/orders"}}} =
live(conn, ~p"/admin/orders/#{fake_id}")
end
test "shows fulfilment card", %{conn: conn} do
order = order_fixture(payment_status: "paid")
{:ok, _view, html} = live(conn, ~p"/admin/orders/#{order}")
assert html =~ "Fulfilment"
assert html =~ "unfulfilled"
end
test "shows submit button for paid unfulfilled orders", %{conn: conn} do
order = order_fixture(payment_status: "paid")
{:ok, _view, html} = live(conn, ~p"/admin/orders/#{order}")
assert html =~ "Submit to provider"
end
test "shows retry button for failed fulfilment", %{conn: conn} do
order = order_fixture(payment_status: "paid")
{:ok, order} =
Berrypod.Orders.update_fulfilment(order, %{fulfilment_status: "failed"})
{:ok, _view, html} = live(conn, ~p"/admin/orders/#{order}")
assert html =~ "Retry submission"
end
test "shows refresh button for submitted orders", %{conn: conn} do
{order, _v, _p, _c} =
Berrypod.OrdersFixtures.submitted_order_fixture()
{:ok, _view, html} = live(conn, ~p"/admin/orders/#{order}")
assert html =~ "Refresh status"
end
test "shows tracking info when available", %{conn: conn} do
{order, _v, _p, _c} =
Berrypod.OrdersFixtures.submitted_order_fixture()
{:ok, order} =
Berrypod.Orders.update_fulfilment(order, %{
fulfilment_status: "shipped",
tracking_number: "TRACK123",
tracking_url: "https://track.example.com/TRACK123",
shipped_at: DateTime.utc_now() |> DateTime.truncate(:second)
})
{:ok, _view, html} = live(conn, ~p"/admin/orders/#{order}")
assert html =~ "TRACK123"
assert html =~ "https://track.example.com/TRACK123"
end
end
describe "order list fulfilment column" do
setup %{conn: conn, user: user} do
conn = log_in_user(conn, user)
%{conn: conn}
end
test "shows fulfilment badge in table", %{conn: conn} do
order_fixture(payment_status: "paid")
{:ok, _view, html} = live(conn, ~p"/admin/orders")
assert html =~ "Fulfilment"
assert html =~ "unfulfilled"
end
end
end

View File

@@ -0,0 +1,190 @@
defmodule BerrypodWeb.Admin.ProductsTest do
use BerrypodWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
import Berrypod.ProductsFixtures
setup do
user = user_fixture()
conn = provider_connection_fixture(%{provider_type: "printify", name: "Test Shop"})
product = complete_product_fixture(%{provider_connection: conn})
%{user: user, connection: conn, product: product}
end
describe "unauthenticated" do
test "product list redirects to login", %{conn: conn} do
{:error, redirect} = live(conn, ~p"/admin/products")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
test "product detail redirects to login", %{conn: conn, product: product} do
{:error, redirect} = live(conn, ~p"/admin/products/#{product}")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "product list" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "renders product list", %{conn: conn, product: product} do
{:ok, _view, html} = live(conn, ~p"/admin/products")
assert html =~ "Products"
assert html =~ product.title
end
test "shows provider badge", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/products")
assert html =~ "Printify"
end
test "shows category", %{conn: conn, product: product} do
{:ok, _view, html} = live(conn, ~p"/admin/products")
assert html =~ product.category
end
test "links to product detail", %{conn: conn, product: product} do
{:ok, _view, html} = live(conn, ~p"/admin/products")
assert html =~ ~p"/admin/products/#{product}"
end
test "toggle visibility updates product", %{conn: conn, product: product} do
{:ok, view, _html} = live(conn, ~p"/admin/products")
assert product.visible
view
|> element("button[phx-value-id='#{product.id}']")
|> render_click()
updated = Berrypod.Products.get_product(product.id)
refute updated.visible
end
test "filters by visibility", %{conn: conn, product: product} do
Berrypod.Products.update_storefront(product, %{visible: false})
{:ok, view, _html} = live(conn, ~p"/admin/products")
html =
view
|> element("form")
|> render_change(%{"visibility" => "hidden"})
assert html =~ product.title
html =
view
|> element("form")
|> render_change(%{"visibility" => "visible"})
refute html =~ product.title
end
test "filters by category", %{conn: conn, product: product} do
{:ok, view, _html} = live(conn, ~p"/admin/products")
html =
view
|> element("form")
|> render_change(%{"category" => product.category})
assert html =~ product.title
html =
view
|> element("form")
|> render_change(%{"category" => "Nonexistent"})
refute html =~ product.title
end
test "sorts by name", %{conn: conn, product: product} do
{:ok, view, _html} = live(conn, ~p"/admin/products")
html =
view
|> element("form")
|> render_change(%{"sort" => "name_asc"})
assert html =~ product.title
end
test "shows empty state when no products", %{conn: conn, product: product} do
Berrypod.Products.delete_product(product)
{:ok, _view, html} = live(conn, ~p"/admin/products")
assert html =~ "No products yet"
assert html =~ "Connect a provider"
end
end
describe "product detail" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "renders product detail", %{conn: conn, product: product} do
{:ok, _view, html} = live(conn, ~p"/admin/products/#{product}")
assert html =~ product.title
assert html =~ "Details"
assert html =~ "Storefront controls"
assert html =~ "Variants"
end
test "shows provider link", %{conn: conn, product: product} do
{:ok, _view, html} = live(conn, ~p"/admin/products/#{product}")
assert html =~ "Edit on Printify"
end
test "shows view on shop link", %{conn: conn, product: product} do
{:ok, _view, html} = live(conn, ~p"/admin/products/#{product}")
assert html =~ ~p"/products/#{product.slug}"
end
test "shows variant details", %{conn: conn, product: product} do
{:ok, _view, html} = live(conn, ~p"/admin/products/#{product}")
assert html =~ "£25.00"
end
test "saves storefront changes", %{conn: conn, product: product} do
{:ok, view, _html} = live(conn, ~p"/admin/products/#{product}")
view
|> element("form")
|> render_submit(%{"product" => %{"visible" => "false", "category" => "New Category"}})
updated = Berrypod.Products.get_product(product.id)
refute updated.visible
assert updated.category == "New Category"
end
test "redirects on not found", %{conn: conn} do
{:error, {:live_redirect, %{to: path}}} =
live(conn, "/admin/products/00000000-0000-0000-0000-000000000000")
assert path == "/admin/products"
end
test "back link navigates to list", %{conn: conn, product: product} do
{:ok, _view, html} = live(conn, ~p"/admin/products/#{product}")
assert html =~ ~s(href="/admin/products")
assert html =~ "Products"
end
end
end

View File

@@ -0,0 +1,271 @@
defmodule BerrypodWeb.Admin.ProvidersTest do
use BerrypodWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
import Berrypod.ProductsFixtures
import Mox
alias Berrypod.Providers.MockProvider
setup :verify_on_exit!
setup do
user = user_fixture()
%{user: user}
end
# -- Index page --
describe "index - unauthenticated" do
test "redirects to login", %{conn: conn} do
{:error, redirect} = live(conn, ~p"/admin/providers")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "index - empty state" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "shows empty state when no connections exist", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/providers")
assert html =~ "Connect a print-on-demand provider"
end
test "shows connect buttons for both providers", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/providers")
assert has_element?(view, ~s(a[href="/admin/providers/new?type=printify"]))
assert has_element?(view, ~s(a[href="/admin/providers/new?type=printful"]))
end
end
describe "index - with connection" do
setup %{conn: conn, user: user} do
connection =
provider_connection_fixture(%{
provider_type: "printify",
name: "My Printify Shop"
})
%{conn: log_in_user(conn, user), connection: connection}
end
test "lists provider connections", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/providers")
assert html =~ "Printify"
assert html =~ "My Printify Shop"
end
test "shows edit link", %{conn: conn, connection: connection} do
{:ok, view, _html} = live(conn, ~p"/admin/providers")
assert has_element?(view, ~s(a[href="/admin/providers/#{connection.id}/edit"]))
end
test "shows product count", %{conn: conn, connection: connection} do
product_fixture(%{provider_connection: connection})
{:ok, _view, html} = live(conn, ~p"/admin/providers")
assert html =~ "1 product"
end
test "shows never synced warning", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/providers")
assert html =~ "Never synced"
end
end
describe "index - delete" do
setup %{conn: conn, user: user} do
connection =
provider_connection_fixture(%{
provider_type: "printify",
name: "Delete Me Shop"
})
%{conn: log_in_user(conn, user), connection: connection}
end
test "deletes connection and shows flash", %{conn: conn, connection: connection} do
{:ok, view, _html} = live(conn, ~p"/admin/providers")
html = render_click(view, "delete", %{"id" => to_string(connection.id)})
assert html =~ "Provider connection deleted"
refute html =~ "Delete Me Shop"
end
end
describe "index - sync" do
setup %{conn: conn, user: user} do
Application.put_env(:berrypod, :provider_modules, %{
"printify" => MockProvider
})
on_exit(fn -> Application.delete_env(:berrypod, :provider_modules) end)
connection =
provider_connection_fixture(%{
provider_type: "printify",
name: "Sync Test Shop"
})
%{conn: log_in_user(conn, user), connection: connection}
end
test "starts sync and shows flash", %{conn: conn, connection: connection} do
stub(MockProvider, :fetch_products, fn _conn -> {:ok, []} end)
{:ok, view, _html} = live(conn, ~p"/admin/providers")
html = render_click(view, "sync", %{"id" => to_string(connection.id)})
assert html =~ "Sync started"
end
end
# -- Form page --
describe "form - new" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "renders new Printify form", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/providers/new?type=printify")
assert html =~ "Connect to Printify"
assert html =~ "Printify API key"
assert html =~ "Log in to Printify"
end
test "renders new Printful form", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/providers/new?type=printful")
assert html =~ "Connect to Printful"
assert html =~ "Printful API key"
assert html =~ "Log in to Printful"
end
test "defaults to Printify when no type param", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/providers/new")
assert html =~ "Connect to Printify"
end
test "test connection shows error when no api key", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/providers/new")
html = render_click(view, "test_connection")
assert html =~ "Please enter your API key"
end
test "saves new connection", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/providers/new")
{:ok, _view, html} =
view
|> form("#provider-form", %{
"provider_connection" => %{
"api_key" => "test_key_123"
}
})
|> render_submit()
|> follow_redirect(conn, ~p"/admin/settings")
assert html =~ "Connected to Printify"
end
end
describe "form - test connection" do
setup %{conn: conn, user: user} do
Application.put_env(:berrypod, :provider_modules, %{
"printify" => MockProvider
})
on_exit(fn -> Application.delete_env(:berrypod, :provider_modules) end)
%{conn: log_in_user(conn, user)}
end
test "shows success when connection is valid", %{conn: conn} do
expect(MockProvider, :test_connection, fn _conn ->
{:ok, %{shop_name: "My Printify Shop", shop_id: 12345}}
end)
{:ok, view, _html} = live(conn, ~p"/admin/providers/new")
# Validate first to set pending_api_key
view
|> form("#provider-form", %{
"provider_connection" => %{"api_key" => "valid_key_123"}
})
|> render_change()
html = render_click(view, "test_connection")
assert html =~ "Connected to My Printify Shop"
end
test "shows error when connection fails", %{conn: conn} do
expect(MockProvider, :test_connection, fn _conn ->
{:error, :unauthorized}
end)
{:ok, view, _html} = live(conn, ~p"/admin/providers/new")
view
|> form("#provider-form", %{
"provider_connection" => %{"api_key" => "bad_key"}
})
|> render_change()
html = render_click(view, "test_connection")
assert html =~ "doesn&#39;t seem to be valid"
end
end
describe "form - edit" do
setup %{conn: conn, user: user} do
connection =
provider_connection_fixture(%{
provider_type: "printify",
name: "Edit Me Shop"
})
%{conn: log_in_user(conn, user), connection: connection}
end
test "renders edit form", %{conn: conn, connection: connection} do
{:ok, _view, html} = live(conn, ~p"/admin/providers/#{connection.id}/edit")
assert html =~ "Printify settings"
assert html =~ "Connection enabled"
assert html =~ "Save changes"
end
test "saves changes", %{conn: conn, connection: connection} do
{:ok, view, _html} = live(conn, ~p"/admin/providers/#{connection.id}/edit")
{:ok, _view, html} =
view
|> form("#provider-form", %{
"provider_connection" => %{"enabled" => "true"}
})
|> render_submit()
|> follow_redirect(conn, ~p"/admin/settings")
assert html =~ "Settings saved"
end
end
end

View File

@@ -0,0 +1,256 @@
defmodule BerrypodWeb.Admin.SettingsTest do
use BerrypodWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
import Berrypod.ProductsFixtures
alias Berrypod.Accounts
alias Berrypod.Settings
setup do
user = user_fixture()
%{user: user}
end
describe "shop status toggle" do
setup %{conn: conn, user: user} do
conn = log_in_user(conn, user)
%{conn: conn}
end
test "shows offline status by default", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/settings")
assert html =~ "Offline"
assert html =~ "coming soon"
assert html =~ "Go live"
end
test "can go live", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
html = render_click(view, "toggle_site_live")
assert html =~ "Shop is now live"
assert html =~ "Live"
assert html =~ "Take offline"
assert Settings.site_live?()
end
test "can take offline after going live", %{conn: conn} do
{:ok, _} = Settings.set_site_live(true)
{:ok, view, _html} = live(conn, ~p"/admin/settings")
html = render_click(view, "toggle_site_live")
assert html =~ "Shop taken offline"
assert html =~ "Offline"
assert html =~ "Go live"
refute Settings.site_live?()
end
end
describe "unauthenticated" do
test "redirects to login", %{conn: conn} do
{:error, redirect} = live(conn, ~p"/admin/settings")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "authenticated - not configured" do
setup %{conn: conn, user: user} do
conn = log_in_user(conn, user)
%{conn: conn}
end
test "renders setup form when Stripe is not configured", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/settings")
assert html =~ "Settings"
assert html =~ "Not connected"
assert html =~ "Connect Stripe"
assert html =~ "Stripe dashboard"
end
test "shows error for empty API key", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
html =
view
|> form(~s(form[phx-submit="connect_stripe"]), %{stripe: %{api_key: ""}})
|> render_submit()
assert html =~ "Please enter your Stripe secret key"
end
end
describe "authenticated - connected (localhost)" do
setup %{conn: conn, user: user} do
# Pre-configure a Stripe API key
Settings.put_secret("stripe_api_key", "sk_test_simulated_key_12345")
conn = log_in_user(conn, user)
%{conn: conn}
end
test "renders dev mode view with CLI instructions", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/settings")
assert html =~ "Dev mode"
assert html =~ "sk_test_•••345"
assert html =~ "stripe listen"
assert html =~ "Webhook signing secret"
end
test "saves manual signing secret", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
html =
view
|> form(~s(form[phx-submit="save_signing_secret"]), %{
webhook: %{signing_secret: "whsec_test_manual_456"}
})
|> render_submit()
assert html =~ "Webhook signing secret saved"
assert html =~ "whsec_te•••456"
end
test "shows error for empty signing secret", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
html =
view
|> form(~s(form[phx-submit="save_signing_secret"]), %{webhook: %{signing_secret: ""}})
|> render_submit()
assert html =~ "Please enter a signing secret"
end
test "disconnect clears configuration", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
html = render_click(view, "disconnect_stripe")
assert html =~ "Stripe disconnected"
assert html =~ "Not connected"
assert html =~ "Connect Stripe"
refute Settings.has_secret?("stripe_api_key")
end
end
describe "products section" do
setup %{conn: conn, user: user} do
conn = log_in_user(conn, user)
%{conn: conn}
end
test "shows connect button when no provider connected", %{conn: conn} do
{:ok, view, html} = live(conn, ~p"/admin/settings")
assert html =~ "Products"
assert html =~ "Not connected"
assert has_element?(view, ~s(a[href="/admin/providers"]), "Connect a provider")
end
test "shows connection info when provider connected", %{conn: conn} do
conn_record = provider_connection_fixture(%{name: "Test Shop"})
product_fixture(%{provider_connection: conn_record})
{:ok, _view, html} = live(conn, ~p"/admin/settings")
assert html =~ "Connected"
assert html =~ "Test Shop"
assert html =~ "Sync products"
end
end
describe "account section" do
setup %{conn: conn, user: user} do
conn = log_in_user(conn, user)
%{conn: conn, user: user}
end
test "renders email and password forms", %{conn: conn, user: user} do
{:ok, view, html} = live(conn, ~p"/admin/settings")
assert html =~ "Account"
assert html =~ user.email
assert has_element?(view, "#email_form")
assert has_element?(view, "#password_form")
end
test "validates email change", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
result =
view
|> element("#email_form")
|> render_change(%{"user" => %{"email" => "with spaces"}})
assert result =~ "must have the @ sign and no spaces"
end
test "submits email change", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
result =
view
|> form("#email_form", %{"user" => %{"email" => unique_user_email()}})
|> render_submit()
assert result =~ "A link to confirm your email"
end
test "validates password", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
result =
view
|> element("#password_form")
|> render_change(%{
"user" => %{
"password" => "short",
"password_confirmation" => "mismatch"
}
})
assert result =~ "should be at least 12 character(s)"
end
test "submits valid password change", %{conn: conn, user: user} do
new_password = valid_user_password()
{:ok, view, _html} = live(conn, ~p"/admin/settings")
form =
form(view, "#password_form", %{
"user" => %{
"email" => user.email,
"password" => new_password,
"password_confirmation" => new_password
}
})
render_submit(form)
new_password_conn = follow_trigger_action(form, conn)
assert redirected_to(new_password_conn) == ~p"/admin/settings"
assert Accounts.get_user_by_email_and_password(user.email, new_password)
end
end
describe "advanced section" do
setup %{conn: conn, user: user} do
conn = log_in_user(conn, user)
%{conn: conn}
end
test "shows links to system tools", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
assert has_element?(view, ~s(a[href="/admin/dashboard"]), "System dashboard")
assert has_element?(view, ~s(a[href="/admin/errors"]), "Error tracker")
end
end
end

View File

@@ -0,0 +1,216 @@
defmodule BerrypodWeb.Admin.ThemeTest do
use BerrypodWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
alias Berrypod.Settings
setup do
user = user_fixture()
%{user: user}
end
describe "Index (unauthenticated)" do
test "redirects to login when not authenticated", %{conn: conn} do
{:error, redirect} = live(conn, ~p"/admin/theme")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "Index (authenticated)" do
setup %{conn: conn, user: user} do
conn = log_in_user(conn, user)
%{conn: conn}
end
test "renders theme editor page", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/theme")
assert html =~ "Theme Studio"
assert html =~ "preset"
end
test "displays all 8 presets", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/theme")
assert html =~ "gallery"
assert html =~ "studio"
assert html =~ "boutique"
assert html =~ "bold"
assert html =~ "playful"
assert html =~ "minimal"
assert html =~ "night"
assert html =~ "classic"
end
test "displays current theme settings", %{conn: conn} do
{:ok, _settings} = Settings.apply_preset(:gallery)
{:ok, _view, html} = live(conn, ~p"/admin/theme")
assert html =~ "warm"
assert html =~ "editorial"
assert html =~ "soft"
assert html =~ "spacious"
end
test "displays generated CSS in preview", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/theme")
# CSS generator outputs accent colors and layout variables for shop pages
assert html =~ ".themed {"
assert html =~ "--t-accent-h:"
end
test "applies preset and updates preview", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/theme")
html =
view
|> element("button", "gallery")
|> render_click()
theme_settings = Settings.get_theme_settings()
assert theme_settings.mood == "warm"
assert theme_settings.typography == "editorial"
assert html =~ "warm"
assert html =~ "editorial"
end
test "switches preview page", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/theme")
html =
view
|> element("button", "Collection")
|> render_click()
assert html =~ "All Products"
end
test "theme settings are saved when applying a preset", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/theme")
# Apply a preset
view
|> element("button", "gallery")
|> render_click()
# Verify settings were persisted
theme_settings = Settings.get_theme_settings()
assert theme_settings.mood == "warm"
assert theme_settings.typography == "editorial"
end
test "all preview page buttons are present", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/theme")
assert html =~ "Home"
assert html =~ "Collection"
assert html =~ "Product"
assert html =~ "Cart"
assert html =~ "About"
assert html =~ "Contact"
assert html =~ "404"
end
test "mood customization buttons work", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/theme")
# Click the "dark" mood button
html =
view
|> element("button[phx-value-setting_value='dark']")
|> render_click()
# Verify the setting was updated
theme_settings = Settings.get_theme_settings()
assert theme_settings.mood == "dark"
assert html =~ "dark"
end
test "shape customization buttons work", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/theme")
# Click the "round" shape button
view
|> element("button[phx-value-setting_value='round']")
|> render_click()
# Verify the setting was updated
theme_settings = Settings.get_theme_settings()
assert theme_settings.shape == "round"
end
test "density customization buttons work", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/theme")
# Click the "compact" density button
view
|> element("button[phx-value-setting_value='compact']")
|> render_click()
# Verify the setting was updated
theme_settings = Settings.get_theme_settings()
assert theme_settings.density == "compact"
end
test "grid columns customization buttons work", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/theme")
# Click the "2 columns" grid columns button
view
|> element("button", "2 columns")
|> render_click()
# Verify the setting was updated
theme_settings = Settings.get_theme_settings()
assert theme_settings.grid_columns == "2"
end
test "typography customization buttons work", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/theme")
# Click the "modern" typography button
view
|> element("button[phx-value-setting_value='modern']")
|> render_click()
# Verify the setting was updated
theme_settings = Settings.get_theme_settings()
assert theme_settings.typography == "modern"
end
test "header layout customization buttons work", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/theme")
# Click the "centered" header layout button
view
|> element("button[phx-value-setting_value='centered']")
|> render_click()
# Verify the setting was updated
theme_settings = Settings.get_theme_settings()
assert theme_settings.header_layout == "centered"
end
test "CSS regenerates when settings change", %{conn: conn} do
{:ok, view, html} = live(conn, ~p"/admin/theme")
# Capture initial CSS
initial_css = html
# Change a setting
new_html =
view
|> element("button[phx-value-setting_value='dark']")
|> render_click()
# Verify CSS has changed
refute initial_css == new_html
assert new_html =~ "--t-accent-h:"
end
end
end

View File

@@ -0,0 +1,118 @@
defmodule BerrypodWeb.Auth.ConfirmationTest do
use BerrypodWeb.ConnCase
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
alias Berrypod.Accounts
setup do
%{unconfirmed_user: unconfirmed_user_fixture(), confirmed_user: user_fixture()}
end
describe "Confirm user" do
test "renders confirmation page for unconfirmed user", %{conn: conn, unconfirmed_user: user} do
token =
extract_user_token(fn url ->
Accounts.deliver_login_instructions(user, url)
end)
{:ok, _lv, html} = live(conn, ~p"/users/log-in/#{token}")
assert html =~ "Confirm and stay logged in"
end
test "renders login page for confirmed user", %{conn: conn, confirmed_user: user} do
token =
extract_user_token(fn url ->
Accounts.deliver_login_instructions(user, url)
end)
{:ok, _lv, html} = live(conn, ~p"/users/log-in/#{token}")
refute html =~ "Confirm my account"
assert html =~ "Keep me logged in on this device"
end
test "renders login page for already logged in user", %{conn: conn, confirmed_user: user} do
conn = log_in_user(conn, user)
token =
extract_user_token(fn url ->
Accounts.deliver_login_instructions(user, url)
end)
{:ok, _lv, html} = live(conn, ~p"/users/log-in/#{token}")
refute html =~ "Confirm my account"
assert html =~ "Log in"
end
test "confirms the given token once", %{conn: conn, unconfirmed_user: user} do
token =
extract_user_token(fn url ->
Accounts.deliver_login_instructions(user, url)
end)
{:ok, lv, _html} = live(conn, ~p"/users/log-in/#{token}")
form = form(lv, "#confirmation_form", %{"user" => %{"token" => token}})
render_submit(form)
conn = follow_trigger_action(form, conn)
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
"User confirmed successfully"
assert Accounts.get_user!(user.id).confirmed_at
# we are logged in now
assert get_session(conn, :user_token)
assert redirected_to(conn) == ~p"/admin"
# log out, new conn
conn = build_conn()
{:ok, _lv, html} =
live(conn, ~p"/users/log-in/#{token}")
|> follow_redirect(conn, ~p"/users/log-in")
assert html =~ "Magic link is invalid or it has expired"
end
test "logs confirmed user in without changing confirmed_at", %{
conn: conn,
confirmed_user: user
} do
token =
extract_user_token(fn url ->
Accounts.deliver_login_instructions(user, url)
end)
{:ok, lv, _html} = live(conn, ~p"/users/log-in/#{token}")
form = form(lv, "#login_form", %{"user" => %{"token" => token}})
render_submit(form)
conn = follow_trigger_action(form, conn)
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
"Welcome back!"
assert Accounts.get_user!(user.id).confirmed_at == user.confirmed_at
# log out, new conn
conn = build_conn()
{:ok, _lv, html} =
live(conn, ~p"/users/log-in/#{token}")
|> follow_redirect(conn, ~p"/users/log-in")
assert html =~ "Magic link is invalid or it has expired"
end
test "raises error for invalid token", %{conn: conn} do
{:ok, _lv, html} =
live(conn, ~p"/users/log-in/invalid-token")
|> follow_redirect(conn, ~p"/users/log-in")
assert html =~ "Magic link is invalid or it has expired"
end
end
end

View File

@@ -0,0 +1,109 @@
defmodule BerrypodWeb.Auth.LoginTest do
use BerrypodWeb.ConnCase
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
describe "login page" do
test "renders login page", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/users/log-in")
assert html =~ "Log in"
assert html =~ "Sign up"
assert html =~ "Log in with email"
end
end
describe "user login - magic link" do
test "sends magic link email when user exists", %{conn: conn} do
user = user_fixture()
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
{:ok, _lv, html} =
form(lv, "#login_form_magic", user: %{email: user.email})
|> render_submit()
|> follow_redirect(conn, ~p"/users/log-in")
assert html =~ "If your email is in our system"
assert Berrypod.Repo.get_by!(Berrypod.Accounts.UserToken, user_id: user.id).context ==
"login"
end
test "does not disclose if user is registered", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
{:ok, _lv, html} =
form(lv, "#login_form_magic", user: %{email: "idonotexist@example.com"})
|> render_submit()
|> follow_redirect(conn, ~p"/users/log-in")
assert html =~ "If your email is in our system"
end
end
describe "user login - password" do
test "redirects if user logs in with valid credentials", %{conn: conn} do
user = user_fixture() |> set_password()
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
form =
form(lv, "#login_form_password",
user: %{email: user.email, password: valid_user_password(), remember_me: true}
)
conn = submit_form(form, conn)
assert redirected_to(conn) == ~p"/admin"
end
test "redirects to login page with a flash error if credentials are invalid", %{
conn: conn
} do
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
form =
form(lv, "#login_form_password", user: %{email: "test@email.com", password: "123456"})
render_submit(form, %{user: %{remember_me: true}})
conn = follow_trigger_action(form, conn)
assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
assert redirected_to(conn) == ~p"/users/log-in"
end
end
describe "login navigation" do
test "redirects to registration page when the Register button is clicked", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
{:ok, _login_live, login_html} =
lv
|> element("main a", "Sign up")
|> render_click()
|> follow_redirect(conn, ~p"/users/register")
assert login_html =~ "Register"
end
end
describe "re-authentication (sudo mode)" do
setup %{conn: conn} do
user = user_fixture()
%{user: user, conn: log_in_user(conn, user)}
end
test "shows login page with email filled in", %{conn: conn, user: user} do
{:ok, _lv, html} = live(conn, ~p"/users/log-in")
assert html =~ "You need to reauthenticate"
refute html =~ "Register"
assert html =~ "Log in with email"
assert html =~
~s(<input type="email" name="user[email]" id="login_form_magic_email" value="#{user.email}")
end
end
end

View File

@@ -0,0 +1,90 @@
defmodule BerrypodWeb.Auth.RegistrationTest do
use BerrypodWeb.ConnCase
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
describe "Registration page" do
test "renders registration page when no admin exists", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/users/register")
assert html =~ "Register"
assert html =~ "Log in"
end
test "redirects to login when admin already exists", %{conn: conn} do
user_fixture()
assert {:error,
{:redirect, %{to: "/users/log-in", flash: %{"error" => "Registration is closed"}}}} =
live(conn, ~p"/users/register")
end
test "redirects if already logged in", %{conn: conn} do
result =
conn
|> log_in_user(user_fixture())
|> live(~p"/users/register")
|> follow_redirect(conn, ~p"/admin")
assert {:ok, _conn} = result
end
test "renders errors for invalid data", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/users/register")
result =
lv
|> element("#registration_form")
|> render_change(user: %{"email" => "with spaces"})
assert result =~ "Register"
assert result =~ "must have the @ sign and no spaces"
end
end
describe "register user" do
test "creates account but does not log in", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/users/register")
email = unique_user_email()
form = form(lv, "#registration_form", user: valid_user_attributes(email: email))
{:ok, _lv, html} =
render_submit(form)
|> follow_redirect(conn, ~p"/users/log-in")
assert html =~
~r/An email was sent to .*, please access it to confirm your account/
end
test "renders errors for duplicated email", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/users/register")
user = user_fixture(%{email: "test@email.com"})
result =
lv
|> form("#registration_form",
user: %{"email" => user.email}
)
|> render_submit()
assert result =~ "has already been taken"
end
end
describe "registration navigation" do
test "redirects to login page when the Log in button is clicked", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/users/register")
{:ok, _login_live, login_html} =
lv
|> element("main a", "Log in")
|> render_click()
|> follow_redirect(conn, ~p"/users/log-in")
assert login_html =~ "Log in"
end
end
end

View File

@@ -0,0 +1,68 @@
defmodule BerrypodWeb.Auth.SettingsTest do
use BerrypodWeb.ConnCase
alias Berrypod.Accounts
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
describe "settings redirect" do
test "redirects to admin settings when logged in", %{conn: conn} do
conn = log_in_user(conn, user_fixture())
assert {:error, {:redirect, %{to: "/admin/settings"}}} = live(conn, ~p"/users/settings")
end
test "redirects to login when not logged in", %{conn: conn} do
assert {:error, redirect} = live(conn, ~p"/users/settings")
assert {:redirect, %{to: path, flash: flash}} = redirect
assert path == ~p"/users/log-in"
assert %{"error" => "You must log in to access this page."} = flash
end
end
describe "confirm email" do
setup %{conn: conn} do
user = user_fixture()
email = unique_user_email()
token =
extract_user_token(fn url ->
Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
end)
%{conn: log_in_user(conn, user), token: token, email: email, user: user}
end
test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
assert {:redirect, %{to: "/admin/settings", flash: flash}} = redirect
assert %{"info" => "Email changed successfully."} = flash
refute Accounts.get_user_by_email(user.email)
assert Accounts.get_user_by_email(email)
# use confirm token again
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
assert {:redirect, %{to: "/admin/settings", flash: flash}} = redirect
assert %{"error" => "Email change link is invalid or it has expired."} = flash
end
test "does not update email with invalid token", %{conn: conn, user: user} do
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/oops")
assert {:redirect, %{to: "/admin/settings", flash: flash}} = redirect
assert %{"error" => "Email change link is invalid or it has expired."} = flash
assert Accounts.get_user_by_email(user.email)
end
test "redirects if user is not logged in", %{token: token} do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
assert {:redirect, %{to: path, flash: flash}} = redirect
assert path == ~p"/users/log-in"
assert %{"error" => message} = flash
assert message == "You must log in to access this page."
end
end
end

View 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

View 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

View 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

View 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 &amp; 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 &amp; 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 =~ "58 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 &amp; 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

View 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

View 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

View 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

View File

@@ -0,0 +1,191 @@
defmodule BerrypodWeb.ThemeCSSConsistencyTest do
@moduledoc """
Tests that verify CSS works correctly for both the theme editor
preview and the shop pages using the shared .themed class.
Architecture:
- Both shop pages and preview use .themed class for shared styles
- Theme editor uses .preview-frame[data-*] selectors for live switching (in admin.css)
- Shop pages get theme values via inline CSS from CSSGenerator (shop.css)
- Component styles use .themed for shared styling (theme-layer2-attributes.css)
"""
use BerrypodWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
alias Berrypod.Settings
setup do
user = user_fixture()
%{user: user}
end
describe "CSS selector consistency" do
test "shop home page has .themed with data attributes", %{conn: conn, user: user} do
{:ok, _view, html} = live(log_in_user(conn, user), ~p"/")
# Verify themed element exists with theme data attributes
assert html =~ ~r/<div[^>]*class="themed/
assert html =~ ~r/data-mood="/
assert html =~ ~r/data-typography="/
assert html =~ ~r/data-shape="/
assert html =~ ~r/data-density="/
end
test "theme editor has .themed with data attributes", %{conn: conn, user: user} do
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/admin/theme")
# Verify themed element exists in preview-frame with theme data attributes
assert html =~ ~r/<div[^>]*class="themed/
assert html =~ ~r/data-mood="/
assert html =~ ~r/data-typography="/
assert html =~ ~r/data-shape="/
assert html =~ ~r/data-density="/
end
test "shop page uses same theme settings as preview", %{conn: conn, user: user} do
# Set a specific theme configuration
{:ok, _settings} = Settings.apply_preset(:night)
# Check shop page (logged in since site_live is false by default)
conn = log_in_user(conn, user)
{:ok, _view, shop_html} = live(conn, ~p"/")
# Check preview (already authenticated)
{:ok, _view, preview_html} = live(conn, ~p"/admin/theme")
# Extract data-mood values from both
[_, shop_mood] = Regex.run(~r/data-mood="([^"]+)"/, shop_html)
[_, preview_mood] = Regex.run(~r/data-mood="([^"]+)"/, preview_html)
# They should match
assert shop_mood == preview_mood
assert shop_mood == "dark"
end
test "theme settings changes are reflected on shop page", %{conn: conn, user: user} do
conn = log_in_user(conn, user)
# Start with minimal preset (neutral mood)
{:ok, _settings} = Settings.apply_preset(:minimal)
{:ok, _view, html} = live(conn, ~p"/")
assert html =~ ~s(data-mood="neutral")
# Change to night preset (dark mood)
{:ok, _settings} = Settings.apply_preset(:night)
{:ok, _view, html} = live(conn, ~p"/")
assert html =~ ~s(data-mood="dark")
# Change to gallery preset (warm mood)
{:ok, _settings} = Settings.apply_preset(:gallery)
{:ok, _view, html} = live(conn, ~p"/")
assert html =~ ~s(data-mood="warm")
end
end
describe "CSS file structure" do
test "admin.css has .preview-frame variant selectors for theme editor" do
css_path = Path.join([File.cwd!(), "assets", "css", "admin.css"])
css_content = File.read!(css_path)
# Variant selectors are editor-only (.preview-frame) using CSS nesting
# The file uses &[data-*] syntax inside .preview-frame { }
# These rules are in admin.css (admin only), not shop.css
assert css_content =~ ".preview-frame"
assert css_content =~ "&[data-mood=\"dark\"]"
assert css_content =~ "&[data-mood=\"warm\"]"
assert css_content =~ "&[data-typography=\"modern\"]"
assert css_content =~ "&[data-shape=\"sharp\"]"
end
test "theme-layer2-attributes.css has shared .themed component styles" do
css_path = Path.join([File.cwd!(), "assets", "css", "theme-layer2-attributes.css"])
css_content = File.read!(css_path)
# Component styles use .themed for shared styling (both shop and preview)
assert css_content =~ ".themed"
# Uses CSS nesting syntax
assert css_content =~ "& .product-card"
assert css_content =~ "& .filter-pill"
end
end
describe "generated CSS cache" do
test "generated CSS includes ALL theme token categories" do
# Apply a preset with specific values
{:ok, settings} = Settings.apply_preset(:night)
# Generate CSS
css = Berrypod.Theme.CSSGenerator.generate(settings)
# Mood tokens (surface, text, border colors)
assert css =~ "--t-surface-base:"
assert css =~ "--t-text-primary:"
assert css =~ "--t-border-default:"
# Typography tokens
assert css =~ "--t-font-heading:"
assert css =~ "--t-font-body:"
assert css =~ "--t-heading-weight:"
# Shape tokens (border radii)
assert css =~ "--t-radius-sm:"
assert css =~ "--t-radius-button:"
assert css =~ "--t-radius-card:"
# Density tokens
assert css =~ "--t-density:"
assert css =~ "--space-md:"
assert css =~ "--space-lg:"
# Slider-controlled values
assert css =~ "--t-accent-h:"
assert css =~ "--t-accent-s:"
assert css =~ "--t-accent-l:"
assert css =~ "--t-font-size-scale:"
assert css =~ "--t-heading-weight-override:"
end
test "generated CSS uses correct values for dark mood" do
{:ok, settings} = Settings.apply_preset(:night)
css = Berrypod.Theme.CSSGenerator.generate(settings)
# Dark mood should have dark surface colors
assert css =~ "--t-surface-base: #0a0a0a"
assert css =~ "--t-text-primary: #fafafa"
end
test "generated CSS uses correct values for warm mood" do
{:ok, settings} = Settings.apply_preset(:gallery)
css = Berrypod.Theme.CSSGenerator.generate(settings)
# Warm mood should have warm surface colors
assert css =~ "--t-surface-base: #fdf8f3"
end
test "CSS cache is warmed on startup and invalidated on settings change" do
# Ensure cache has content
Berrypod.Theme.CSSCache.warm()
{:ok, css1} = Berrypod.Theme.CSSCache.get()
assert is_binary(css1)
assert css1 =~ "--t-accent-h:"
# Change settings (this should invalidate and rewarm cache)
{:ok, _settings} = Settings.apply_preset(:night)
{:ok, css2} = Berrypod.Theme.CSSCache.get()
assert is_binary(css2)
# The CSS should be different (different accent color)
# Note: this may or may not be true depending on preset colors
assert css2 =~ "--t-accent-h:"
end
end
end