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:
117
test/berrypod_web/live/admin/dashboard_test.exs
Normal file
117
test/berrypod_web/live/admin/dashboard_test.exs
Normal 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
|
||||
98
test/berrypod_web/live/admin/layout_test.exs
Normal file
98
test/berrypod_web/live/admin/layout_test.exs
Normal 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
|
||||
190
test/berrypod_web/live/admin/orders_test.exs
Normal file
190
test/berrypod_web/live/admin/orders_test.exs
Normal 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
|
||||
190
test/berrypod_web/live/admin/products_test.exs
Normal file
190
test/berrypod_web/live/admin/products_test.exs
Normal 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
|
||||
271
test/berrypod_web/live/admin/providers_test.exs
Normal file
271
test/berrypod_web/live/admin/providers_test.exs
Normal 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'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
|
||||
256
test/berrypod_web/live/admin/settings_test.exs
Normal file
256
test/berrypod_web/live/admin/settings_test.exs
Normal 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
|
||||
216
test/berrypod_web/live/admin/theme_test.exs
Normal file
216
test/berrypod_web/live/admin/theme_test.exs
Normal 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
|
||||
118
test/berrypod_web/live/auth/confirmation_test.exs
Normal file
118
test/berrypod_web/live/auth/confirmation_test.exs
Normal 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
|
||||
109
test/berrypod_web/live/auth/login_test.exs
Normal file
109
test/berrypod_web/live/auth/login_test.exs
Normal 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
|
||||
90
test/berrypod_web/live/auth/registration_test.exs
Normal file
90
test/berrypod_web/live/auth/registration_test.exs
Normal 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
|
||||
68
test/berrypod_web/live/auth/settings_test.exs
Normal file
68
test/berrypod_web/live/auth/settings_test.exs
Normal 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
|
||||
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
|
||||
191
test/berrypod_web/live/theme_css_consistency_test.exs
Normal file
191
test/berrypod_web/live/theme_css_consistency_test.exs
Normal 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
|
||||
Reference in New Issue
Block a user