defmodule SimpleshopTheme.SearchTest do use SimpleshopTheme.DataCase, async: false alias SimpleshopTheme.Search import SimpleshopTheme.ProductsFixtures setup do conn = provider_connection_fixture() mountain = product_fixture(%{ provider_connection: conn, title: "Mountain Sunrise Art Print", description: "
A beautiful mountain landscape at dawn.
", category: "Art Prints" }) product_variant_fixture(%{ product: mountain, title: "Small / Navy", options: %{"Size" => "Small", "Color" => "Navy"}, price: 1999 }) ocean = product_fixture(%{ provider_connection: conn, title: "Ocean Waves Notebook", description: "A spiral-bound notebook with ocean wave cover art.", category: "Stationery" }) product_variant_fixture(%{ product: ocean, title: "A5", options: %{"Size" => "A5"}, price: 1299 }) forest = product_fixture(%{ provider_connection: conn, title: "Forest Silhouette T-Shirt", description: "Cotton t-shirt with forest silhouette print.", category: "Apparel" }) product_variant_fixture(%{ product: forest, title: "Large / Black", options: %{"Size" => "Large", "Color" => "Black"}, price: 2999 }) product_variant_fixture(%{ product: forest, title: "Medium / Navy Blue", options: %{"Size" => "Medium", "Color" => "Navy Blue"}, price: 2999 }) # Hidden product — should not appear in search _hidden = product_fixture(%{ provider_connection: conn, title: "Hidden Mountain Poster", description: "This should not be searchable.", category: "Art Prints", visible: false }) Search.rebuild_index() %{mountain: mountain, ocean: ocean, forest: forest} end describe "search/1" do test "finds products by title", %{mountain: mountain} do results = Search.search("mountain") assert length(results) >= 1 assert Enum.any?(results, &(&1.id == mountain.id)) end test "finds products by category", %{mountain: mountain, ocean: ocean} do results = Search.search("art prints") assert Enum.any?(results, &(&1.id == mountain.id)) refute Enum.any?(results, &(&1.id == ocean.id)) end test "finds products by variant attributes", %{forest: forest} do results = Search.search("navy") assert Enum.any?(results, &(&1.id == forest.id)) end test "finds products by description", %{mountain: mountain} do results = Search.search("landscape") assert Enum.any?(results, &(&1.id == mountain.id)) end test "prefix matching on last token", %{mountain: mountain} do results = Search.search("mou") assert Enum.any?(results, &(&1.id == mountain.id)) end test "multi-word query matches all tokens", %{mountain: mountain} do results = Search.search("mountain sunrise") assert Enum.any?(results, &(&1.id == mountain.id)) end test "returns empty list for blank query" do assert Search.search("") == [] assert Search.search(" ") == [] end test "returns empty list for single character" do assert Search.search("a") == [] end test "returns empty list for nil" do assert Search.search(nil) == [] end test "returns empty list for no matches" do assert Search.search("xyznonexistent") == [] end test "special characters don't crash" do assert Search.search("mountain's") == Search.search("mountains") assert Search.search("test & foo") == [] assert Search.search("(brackets)") == [] end test "hidden products are excluded" do results = Search.search("hidden") assert results == [] end test "strips HTML from descriptions", %{mountain: mountain} do results = Search.search("landscape dawn") assert Enum.any?(results, &(&1.id == mountain.id)) end test "results include listing preloads", %{mountain: mountain} do [result | _] = Search.search("mountain sunrise") assert result.id == mountain.id assert Ecto.assoc_loaded?(result.images) end test "title matches rank higher than description matches" do results = Search.search("mountain") # Mountain Sunrise Art Print (title match) should rank above # Forest T-Shirt if it happened to mention "mountain" in description first = List.first(results) assert first.title =~ "Mountain" end end describe "rebuild_index/0" do test "rebuilds from scratch" do # Verify search works before rebuild assert length(Search.search("mountain")) >= 1 # Rebuild and verify still works Search.rebuild_index() assert length(Search.search("mountain")) >= 1 end end describe "index_product/1" do test "indexes a single product", %{ocean: ocean} do # Clear and rebuild without ocean SimpleshopTheme.Repo.query!("DELETE FROM products_search_map") SimpleshopTheme.Repo.query!("DELETE FROM products_search") assert Search.search("ocean") == [] # Index just ocean ocean = SimpleshopTheme.Repo.preload(ocean, [:variants]) Search.index_product(ocean) results = Search.search("ocean") assert length(results) == 1 assert hd(results).id == ocean.id end test "reindexing updates existing entry", %{mountain: mountain} do # Change title and reindex mountain = mountain |> Ecto.Changeset.change(title: "Alpine Sunrise Art Print") |> SimpleshopTheme.Repo.update!() Search.index_product(mountain) 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