wire shop LiveViews to DB queries and improve search UX

Replace PreviewData indirection in all shop LiveViews with direct
Products context queries. Home, collection, product detail and error
pages now query the database. Categories loaded once in ThemeHook.
Cart hydration no longer falls back to mock data. PreviewData kept
only for the theme editor.

Search modal gains keyboard navigation (arrow keys, Enter, Escape),
Cmd+K/Ctrl+K shortcut, full ARIA combobox pattern, LiveView navigate
links, and 150ms debounce. SearchModal JS hook manages selection
state and highlight. search.ex gets transaction safety on reindex
and a public remove_product/1. 10 new integration tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-13 08:27:26 +00:00
parent 037cd168cd
commit 57c3ba0e28
22 changed files with 745 additions and 330 deletions

View File

@@ -288,15 +288,27 @@ defmodule SimpleshopTheme.Products.ProductTest do
product = %{
provider_data: %{
"options" => [
%{"name" => "Size", "values" => [%{"title" => "S"}, %{"title" => "M"}]},
%{"name" => "Color", "values" => [%{"title" => "Red"}]}
%{
"name" => "Size",
"type" => "size",
"values" => [%{"title" => "S"}, %{"title" => "M"}]
},
%{
"name" => "Color",
"type" => "color",
"values" => [%{"title" => "Red", "colors" => ["#FF0000"]}]
}
]
}
}
types = Product.option_types(product)
assert length(types) == 2
assert hd(types) == %{name: "Size", values: ["S", "M"]}
assert hd(types) == %{name: "Size", type: :size, values: [%{title: "S"}, %{title: "M"}]}
color_type = Enum.at(types, 1)
assert color_type.type == :color
assert hd(color_type.values) == %{title: "Red", hex: "#FF0000"}
end
test "returns empty list when no provider_data" do

View File

@@ -196,4 +196,18 @@ defmodule SimpleshopTheme.SearchTest do
assert Search.search("alpine") != []
end
end
describe "remove_product/1" do
test "removes a product from the index", %{ocean: ocean} do
assert Search.search("ocean") != []
Search.remove_product(ocean.id)
assert Search.search("ocean") == []
end
test "is a no-op for unindexed product" do
assert Search.remove_product(-1) == :ok
end
end
end

View File

@@ -3,13 +3,44 @@ defmodule SimpleshopThemeWeb.Shop.CollectionTest do
import Phoenix.LiveViewTest
import SimpleshopTheme.AccountsFixtures
import SimpleshopTheme.ProductsFixtures
alias SimpleshopTheme.Theme.PreviewData
alias SimpleshopTheme.Products
setup do
user_fixture()
{:ok, _} = SimpleshopTheme.Settings.set_site_live(true)
:ok
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
@@ -20,29 +51,22 @@ defmodule SimpleshopThemeWeb.Shop.CollectionTest do
end
test "renders collection page for specific category", %{conn: conn} do
category = List.first(PreviewData.categories())
{:ok, _view, html} = live(conn, ~p"/collections/#{category.slug}")
{:ok, _view, html} = live(conn, ~p"/collections/art-prints")
assert html =~ category.name
assert html =~ "Art Prints"
end
test "displays products", %{conn: conn} do
test "displays products", %{conn: conn, print: print} do
{:ok, _view, html} = live(conn, ~p"/collections/all")
products = PreviewData.products()
first_product = List.first(products)
assert html =~ first_product.title
assert html =~ print.title
end
test "displays category filter buttons", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/collections/all")
categories = PreviewData.categories()
for category <- categories do
assert html =~ category.name
end
assert html =~ "Art Prints"
assert html =~ "Apparel"
end
test "displays sort dropdown", %{conn: conn} do
@@ -64,15 +88,11 @@ defmodule SimpleshopThemeWeb.Shop.CollectionTest do
assert flash["error"] == "Collection not found"
end
test "filters products by category", %{conn: conn} do
category = List.first(PreviewData.categories())
{:ok, _view, html} = live(conn, ~p"/collections/#{category.slug}")
test "filters products by category", %{conn: conn, print: print, shirt: shirt} do
{:ok, _view, html} = live(conn, ~p"/collections/art-prints")
products = PreviewData.products_by_category(category.slug)
for product <- products do
assert html =~ product.title
end
assert html =~ print.title
refute html =~ shirt.title
end
end
@@ -140,8 +160,7 @@ defmodule SimpleshopThemeWeb.Shop.CollectionTest do
test "category links preserve sort order", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/collections/all?sort=price_desc")
category = List.first(PreviewData.categories())
assert html =~ "/collections/#{category.slug}?sort=price_desc"
assert html =~ "/collections/art-prints?sort=price_desc"
end
test "All button link preserves sort order", %{conn: conn} do
@@ -153,9 +172,8 @@ defmodule SimpleshopThemeWeb.Shop.CollectionTest do
test "featured sort does not include query param in links", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/collections/all")
category = List.first(PreviewData.categories())
assert html =~ ~s(href="/collections/#{category.slug}")
refute html =~ "/collections/#{category.slug}?sort=featured"
assert html =~ ~s(href="/collections/art-prints")
refute html =~ "/collections/art-prints?sort=featured"
end
end
end

View File

@@ -3,13 +3,27 @@ defmodule SimpleshopThemeWeb.Shop.HomeTest do
import Phoenix.LiveViewTest
import SimpleshopTheme.AccountsFixtures
alias SimpleshopTheme.Theme.PreviewData
import SimpleshopTheme.ProductsFixtures
setup do
user_fixture()
{:ok, _} = SimpleshopTheme.Settings.set_site_live(true)
:ok
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
SimpleshopTheme.Products.recompute_cached_fields(product)
%{product: product}
end
describe "Home page" do
@@ -25,25 +39,17 @@ defmodule SimpleshopThemeWeb.Shop.HomeTest do
assert html =~ "Shop the collection"
end
test "renders category navigation", %{conn: conn} do
test "renders category navigation with real categories", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/")
categories = PreviewData.categories()
for category <- Enum.take(categories, 3) do
assert html =~ category.name
end
assert html =~ "Art Prints"
end
test "renders featured products section", %{conn: conn} do
test "renders featured products section", %{conn: conn, product: product} do
{:ok, _view, html} = live(conn, ~p"/")
assert html =~ "Featured products"
products = PreviewData.products()
first_product = List.first(products)
assert html =~ first_product.title
assert html =~ product.title
end
test "renders image and text section", %{conn: conn} do
@@ -56,8 +62,7 @@ defmodule SimpleshopThemeWeb.Shop.HomeTest do
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"
assert html =~ "Store Name"
end
test "renders footer with links", %{conn: conn} do

View File

@@ -3,142 +3,273 @@ defmodule SimpleshopThemeWeb.Shop.ProductShowTest do
import Phoenix.LiveViewTest
import SimpleshopTheme.AccountsFixtures
import SimpleshopTheme.ProductsFixtures
alias SimpleshopTheme.Theme.PreviewData
alias SimpleshopTheme.Products
setup do
user_fixture()
{:ok, _} = SimpleshopTheme.Settings.set_site_live(true)
:ok
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} do
product = List.first(PreviewData.products())
{:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
test "renders product page with product name", %{conn: conn, print: print} do
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
assert html =~ product.title
assert html =~ print.title
end
test "renders product description", %{conn: conn} do
product = List.first(PreviewData.products())
{:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
test "renders product description", %{conn: conn, print: print} do
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
assert html =~ product.description
assert html =~ print.description
end
test "renders product price", %{conn: conn} do
product = List.first(PreviewData.products())
{:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
test "renders product price", %{conn: conn, print: print} do
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
assert html =~ SimpleshopTheme.Cart.format_price(product.cheapest_price)
# Cheapest variant is 8x10 at £19.99
assert html =~ "£19.99"
end
test "renders breadcrumb with category link", %{conn: conn} do
product = List.first(PreviewData.products())
{:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
test "renders breadcrumb with category link", %{conn: conn, print: print} do
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
assert html =~ product.category
assert html =~ "Art Prints"
assert html =~ "/collections/"
end
test "renders add to cart button", %{conn: conn} do
product = List.first(PreviewData.products())
{:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
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} do
product = List.first(PreviewData.products())
{:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
test "renders related products section", %{conn: conn, print: print, related: related} do
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
# Should show other products, not the current one
other_product = Enum.at(PreviewData.products(), 1)
assert html =~ other_product.title
assert html =~ related.title
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")
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 =~ "8×10"
assert html =~ "12×18"
assert html =~ "18×24"
assert html =~ "8x10"
assert html =~ "12x18"
assert html =~ "18x24"
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")
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} 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")
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-value='18×24']")
|> element("button[phx-value-value='18x24']")
|> 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")
test "selecting a colour updates available sizes", %{conn: conn, shirt: shirt} do
{:ok, view, _html} = live(conn, ~p"/products/#{shirt.slug}")
html =
view
|> element("button[aria-label='Select White']")
|> render_click()
# XL should be disabled (unavailable in White)
# XL should be disabled (unavailable in white)
assert html =~ "disabled"
end
test "shows single variant for products with one option", %{conn: conn} do
# Product "2" (Ocean Waves Art Print) has a single size variant
{:ok, _view, html} = live(conn, ~p"/products/2")
test "shows variant for single-variant product", %{conn: conn, related: related} do
{:ok, _view, html} = live(conn, ~p"/products/#{related.slug}")
assert html =~ "12×18"
# 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} do
{:ok, view, _html} = live(conn, ~p"/products/1")
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} do
{:ok, view, _html} = live(conn, ~p"/products/1")
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} do
{:ok, view, _html} = live(conn, ~p"/products/1")
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 should now be 2, decrement no longer disabled
# Quantity now 2, decrement no longer disabled
refute html =~ ~s(phx-click="decrement_quantity" disabled)
end
test "decrement decreases quantity", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/products/1")
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()
@@ -153,8 +284,8 @@ defmodule SimpleshopThemeWeb.Shop.ProductShowTest do
refute html =~ ~s(phx-click="decrement_quantity" disabled)
end
test "quantity resets to 1 after adding to cart", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/products/1")
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()
@@ -173,66 +304,61 @@ defmodule SimpleshopThemeWeb.Shop.ProductShowTest do
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")
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()
# 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")
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()
# The cart drawer should show the item
assert html =~ "Mountain Sunrise Art Print"
assert html =~ "Mountain Sunrise Print"
end
end
describe "Product gallery" do
test "renders carousel with hook and accessibility attrs", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/products/1")
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} do
{:ok, _view, html} = live(conn, ~p"/products/1")
test "renders all gallery images with alt text", %{conn: conn, print: print} do
{:ok, _view, html} = live(conn, ~p"/products/#{print.slug}")
product = List.first(PreviewData.products())
# Each image should have descriptive alt text
assert html =~ "#{product.title} — image 1 of"
assert html =~ "#{product.title} — image 2 of"
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} do
{:ok, view, _html} = live(conn, ~p"/products/1")
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} do
{:ok, view, _html} = live(conn, ~p"/products/1")
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} do
{:ok, view, _html} = live(conn, ~p"/products/1")
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")
@@ -240,20 +366,20 @@ defmodule SimpleshopThemeWeb.Shop.ProductShowTest do
assert has_element?(view, ~s(button[aria-label="Next image"]))
end
test "renders lightbox dialog", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/products/1")
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} do
{:ok, view, _html} = live(conn, ~p"/products/1")
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} do
{:ok, _view, html} = live(conn, ~p"/products/1")
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"
@@ -261,31 +387,25 @@ defmodule SimpleshopThemeWeb.Shop.ProductShowTest do
end
describe "Product gallery edge cases" do
import Phoenix.LiveViewTest
test "single image renders without carousel or dots", %{conn: conn, shirt: shirt} do
{:ok, view, _html} = live(conn, ~p"/products/#{shirt.slug}")
test "single image renders without carousel or dots", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/products/1")
# The live page always has multiple images due to padding, so test
# that the component correctly renders by checking structure exists
# (single-image case would need component-level testing)
assert has_element?(view, ".pdp-gallery-carousel")
# 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} do
product = Enum.at(PreviewData.products(), 1)
{:ok, _view, html} = live(conn, ~p"/products/#{product.id}")
test "product links navigate to correct product page", %{conn: conn, shirt: shirt} do
{:ok, _view, html} = live(conn, ~p"/products/#{shirt.slug}")
assert html =~ product.title
assert html =~ shirt.title
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.title
test "unknown slug redirects to collections", %{conn: conn} do
assert {:error, {:live_redirect, %{to: "/collections/all"}}} =
live(conn, ~p"/products/nonexistent")
end
end
end

View File

@@ -0,0 +1,131 @@
defmodule SimpleshopThemeWeb.Shop.SearchIntegrationTest do
use SimpleshopThemeWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import SimpleshopTheme.AccountsFixtures
import SimpleshopTheme.ProductsFixtures
alias SimpleshopTheme.Search
setup do
user_fixture()
{:ok, _} = SimpleshopTheme.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})
SimpleshopTheme.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})
SimpleshopTheme.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