diff --git a/PROGRESS.md b/PROGRESS.md
index f726af3..a050130 100644
--- a/PROGRESS.md
+++ b/PROGRESS.md
@@ -209,13 +209,15 @@ All 8 items from the plan done. Key wins: ThemeHook eliminated mount duplication
See: [docs/plans/dry-refactor.md](docs/plans/dry-refactor.md) for full analysis and plan
### Shop Page Integration Tests
-**Status:** Follow-up
+**Status:** Complete
-Home, product detail, and cart pages have no LiveView integration tests. Collection and content pages are well-covered (16 and 10 tests respectively). Priority order by logic complexity:
+All shop pages now have LiveView integration tests (612 total):
-1. **Product detail page** — variant selection, add-to-cart, gallery, breadcrumb
-2. **Cart page** — cart items, quantity changes, order summary, checkout link
-3. **Home page** — hero section, featured products, category nav (mostly presentational)
+- **Product detail page** (15 tests) — rendering, breadcrumbs, variant selection, price updates, add-to-cart, related products, fallback for unknown IDs
+- **Cart page** (10 tests) — empty state, item display with DB fixtures, order summary, increment/decrement, remove, checkout button
+- **Home page** (12 tests) — hero section, category nav, featured products, image+text section, navigation links
+- **Collection page** (16 tests, pre-existing) — category filtering, sorting, URL params
+- **Content pages** (10 tests, pre-existing) — about, delivery, privacy, terms
### Page Editor
**Status:** Future (Tier 4)
diff --git a/test/simpleshop_theme_web/live/shop_live/cart_test.exs b/test/simpleshop_theme_web/live/shop_live/cart_test.exs
new file mode 100644
index 0000000..d432af2
--- /dev/null
+++ b/test/simpleshop_theme_web/live/shop_live/cart_test.exs
@@ -0,0 +1,111 @@
+defmodule SimpleshopThemeWeb.ShopLive.CartTest do
+ use SimpleshopThemeWeb.ConnCase, async: false
+
+ import Phoenix.LiveViewTest
+
+ alias SimpleshopTheme.ProductsFixtures
+
+ 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 =~ SimpleshopTheme.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("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("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 =~ "
Cart"
+ end
+ end
+end
diff --git a/test/simpleshop_theme_web/live/shop_live/home_test.exs b/test/simpleshop_theme_web/live/shop_live/home_test.exs
new file mode 100644
index 0000000..7ae6a2e
--- /dev/null
+++ b/test/simpleshop_theme_web/live/shop_live/home_test.exs
@@ -0,0 +1,89 @@
+defmodule SimpleshopThemeWeb.ShopLive.HomeTest do
+ use SimpleshopThemeWeb.ConnCase, async: false
+
+ import Phoenix.LiveViewTest
+
+ alias SimpleshopTheme.Theme.PreviewData
+
+ 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", %{conn: conn} do
+ {:ok, _view, html} = live(conn, ~p"/")
+
+ categories = PreviewData.categories()
+
+ for category <- Enum.take(categories, 3) do
+ assert html =~ category.name
+ end
+ end
+
+ test "renders featured products section", %{conn: conn} do
+ {:ok, _view, html} = live(conn, ~p"/")
+
+ assert html =~ "Featured products"
+
+ products = PreviewData.products()
+ first_product = List.first(products)
+
+ assert html =~ first_product.name
+ 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"/")
+
+ # Header should be present (part of shop_layout)
+ assert html =~ "SimpleShop"
+ 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
diff --git a/test/simpleshop_theme_web/live/shop_live/product_show_test.exs b/test/simpleshop_theme_web/live/shop_live/product_show_test.exs
new file mode 100644
index 0000000..dd2d4a2
--- /dev/null
+++ b/test/simpleshop_theme_web/live/shop_live/product_show_test.exs
@@ -0,0 +1,155 @@
+defmodule SimpleshopThemeWeb.ShopLive.ProductShowTest do
+ use SimpleshopThemeWeb.ConnCase, async: false
+
+ import Phoenix.LiveViewTest
+
+ alias SimpleshopTheme.Theme.PreviewData
+
+ describe "Product detail page" do
+ test "renders product page with product name", %{conn: conn} do
+ product = List.first(PreviewData.products())
+ {:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
+
+ assert html =~ product.name
+ end
+
+ test "renders product description", %{conn: conn} do
+ product = List.first(PreviewData.products())
+ {:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
+
+ assert html =~ product.description
+ end
+
+ test "renders product price", %{conn: conn} do
+ product = List.first(PreviewData.products())
+ {:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
+
+ assert html =~ SimpleshopTheme.Cart.format_price(product.price)
+ end
+
+ test "renders breadcrumb with Home link", %{conn: conn} do
+ product = List.first(PreviewData.products())
+ {:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
+
+ assert html =~ "Home"
+ assert html =~ ~s(href="/")
+ end
+
+ test "renders breadcrumb with category link", %{conn: conn} do
+ product = List.first(PreviewData.products())
+ {:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
+
+ assert html =~ product.category
+ end
+
+ test "renders add to cart button", %{conn: conn} do
+ product = List.first(PreviewData.products())
+ {:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
+
+ assert html =~ "Add to basket"
+ end
+
+ test "renders related products section", %{conn: conn} do
+ product = List.first(PreviewData.products())
+ {:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
+
+ # Should show other products, not the current one
+ other_product = Enum.at(PreviewData.products(), 1)
+ assert html =~ other_product.name
+ end
+ end
+
+ describe "Variant selection" do
+ test "renders variant selectors for product with options", %{conn: conn} do
+ # Product "1" (Mountain Sunrise Art Print) has Size options
+ {:ok, _view, html} = live(conn, ~p"/products/1")
+
+ assert html =~ "Size"
+ assert html =~ "8×10"
+ assert html =~ "12×18"
+ assert html =~ "18×24"
+ end
+
+ test "renders color and size selectors for apparel", %{conn: conn} do
+ # Product "6" (Forest Silhouette T-Shirt) has Color and Size options
+ {:ok, _view, html} = live(conn, ~p"/products/6")
+
+ assert html =~ "Color"
+ assert html =~ "Size"
+ end
+
+ test "selecting a size updates the price", %{conn: conn} do
+ # Product "1" has variants: 8×10 = £19.99, 12×18 = £24.00, 18×24 = £32.00
+ {:ok, view, _html} = live(conn, ~p"/products/1")
+
+ html =
+ view
+ |> element("button[phx-value-value='18×24']")
+ |> render_click()
+
+ assert html =~ "£32.00"
+ end
+
+ test "selecting a colour updates available sizes", %{conn: conn} do
+ # Product "6": White / XL and White / 2XL are unavailable
+ {:ok, view, _html} = live(conn, ~p"/products/6")
+
+ html =
+ view
+ |> element("button[aria-label='Select White']")
+ |> render_click()
+
+ # XL should be disabled (unavailable in White)
+ assert html =~ "disabled"
+ end
+
+ test "shows 'One size' for products without options", %{conn: conn} do
+ # Product "2" (Ocean Waves Art Print) has no option_types
+ {:ok, _view, html} = live(conn, ~p"/products/2")
+
+ assert html =~ "One size"
+ end
+ end
+
+ describe "Add to cart" do
+ test "add to cart opens the cart drawer", %{conn: conn} do
+ {:ok, view, _html} = live(conn, ~p"/products/1")
+
+ html =
+ view
+ |> element("button", "Add to basket")
+ |> render_click()
+
+ # Cart drawer should now be open (the aria live region gets updated)
+ assert html =~ "added to cart"
+ end
+
+ test "add to cart updates cart count", %{conn: conn} do
+ {:ok, view, _html} = live(conn, ~p"/products/1")
+
+ html =
+ view
+ |> element("button", "Add to basket")
+ |> render_click()
+
+ # The cart drawer should show the item
+ assert html =~ "Mountain Sunrise Art Print"
+ end
+ end
+
+ describe "Navigation" do
+ test "product links navigate to correct product page", %{conn: conn} do
+ product = Enum.at(PreviewData.products(), 1)
+ {:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
+
+ assert html =~ product.name
+ end
+
+ test "falls back to first product for unknown ID", %{conn: conn} do
+ {:ok, _view, html} = live(conn, ~p"/products/nonexistent")
+
+ first_product = List.first(PreviewData.products())
+ assert html =~ first_product.name
+ end
+ end
+end