ammocpr/test/ammoprices_web/live/caliber_live/show_test.exs
Graham McIntire 16a28ac01c
Add Target Sports USA scraper, product filtering, and subsonic detection
- Add subsonic boolean field to products with composite filter indexes
- Add TextDetector utility for detecting subsonic/casing from product titles
- Wire TextDetector into SgAmmo (casing + subsonic) and LuckyGunner (subsonic + fallback casing)
- Extend latest_prices_for_caliber with casing, grain_weight, and subsonic filter options
- Add distinct_grain_weights_for_caliber and distinct_casings_for_caliber queries
- Add filter UI with toggleable casing, grain weight, and subsonic buttons
- Add grain weight column and subsonic badge to product table
- Refresh filter options on PubSub price updates for real-time UI
- Implement Target Sports USA scraper with 18 caliber mappings
- Display all prices in dollar format ($0.38 instead of 38¢)
2026-03-11 16:28:04 -05:00

192 lines
8.4 KiB
Elixir

defmodule AmmopricesWeb.CaliberLive.ShowTest do
use AmmopricesWeb.ConnCase
import Ammoprices.Fixtures
import Phoenix.LiveViewTest
setup do
caliber = caliber_fixture(%{name: "9mm Luger", slug: "9mm-luger", category: "handgun"})
retailer = retailer_fixture(%{name: "Lucky Gunner", slug: "lucky-gunner", base_url: "https://www.luckygunner.com"})
%{caliber: caliber, retailer: retailer}
end
describe "CaliberLive.Show" do
test "renders caliber page with title", %{conn: conn, caliber: caliber} do
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
assert has_element?(view, "#caliber-heading")
end
test "renders product listings with stream", %{conn: conn, caliber: caliber, retailer: retailer} do
product = product_fixture(%{caliber: caliber, retailer: retailer, title: "Federal 9mm 115gr"})
now = DateTime.truncate(DateTime.utc_now(), :second)
snapshot_fixture(%{product: product, price_per_round_cents: 28, in_stock: true, recorded_at: now})
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
assert has_element?(view, "#products")
assert has_element?(view, "#products [id]")
end
test "shows empty state when no products", %{conn: conn, caliber: caliber} do
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
assert has_element?(view, "#products-empty")
end
test "filters by in-stock", %{conn: conn, caliber: caliber, retailer: retailer} do
in_stock = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: true, title: "In Stock Product"})
oos = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: false, title: "Out of Stock Product"})
now = DateTime.truncate(DateTime.utc_now(), :second)
snapshot_fixture(%{product: in_stock, price_per_round_cents: 28, in_stock: true, recorded_at: now})
snapshot_fixture(%{product: oos, price_per_round_cents: 22, in_stock: false, recorded_at: now})
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
# Default is in_stock: true
assert has_element?(view, "#filter-in-stock")
end
test "renders price stats banner", %{conn: conn, caliber: caliber, retailer: retailer} do
product = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: true})
now = DateTime.truncate(DateTime.utc_now(), :second)
snapshot_fixture(%{product: product, price_per_round_cents: 25, in_stock: true, recorded_at: now})
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
assert has_element?(view, "#price-stats")
end
test "renders chart container with data attribute", %{conn: conn, caliber: caliber, retailer: retailer} do
product = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: true})
now = DateTime.truncate(DateTime.utc_now(), :second)
snapshot_fixture(%{product: product, price_per_round_cents: 30, in_stock: true, recorded_at: now})
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
assert has_element?(view, "#price-chart-container")
assert has_element?(view, "[data-chart-data]")
end
test "renders time range buttons", %{conn: conn, caliber: caliber} do
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
assert has_element?(view, "#range-7d")
assert has_element?(view, "#range-30d")
assert has_element?(view, "#range-90d")
assert has_element?(view, "#range-1y")
assert has_element?(view, "#range-all")
end
test "changes chart range when range button clicked", %{conn: conn, caliber: caliber, retailer: retailer} do
product = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: true})
now = DateTime.truncate(DateTime.utc_now(), :second)
snapshot_fixture(%{product: product, price_per_round_cents: 30, in_stock: true, recorded_at: now})
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
assert view |> element("#range-7d") |> render_click()
assert has_element?(view, "#price-chart-container")
end
test "renders casing filter buttons", %{conn: conn, caliber: caliber, retailer: retailer} do
now = DateTime.truncate(DateTime.utc_now(), :second)
brass = product_fixture(%{caliber: caliber, retailer: retailer, casing: "brass", in_stock: true})
steel = product_fixture(%{caliber: caliber, retailer: retailer, casing: "steel", in_stock: true})
snapshot_fixture(%{product: brass, price_per_round_cents: 28, in_stock: true, recorded_at: now})
snapshot_fixture(%{product: steel, price_per_round_cents: 20, in_stock: true, recorded_at: now})
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
assert has_element?(view, "#filter-casing-brass")
assert has_element?(view, "#filter-casing-steel")
end
test "renders grain weight filter buttons", %{conn: conn, caliber: caliber, retailer: retailer} do
now = DateTime.truncate(DateTime.utc_now(), :second)
p115 = product_fixture(%{caliber: caliber, retailer: retailer, grain_weight: 115, in_stock: true})
p147 = product_fixture(%{caliber: caliber, retailer: retailer, grain_weight: 147, in_stock: true})
snapshot_fixture(%{product: p115, price_per_round_cents: 25, in_stock: true, recorded_at: now})
snapshot_fixture(%{product: p147, price_per_round_cents: 40, in_stock: true, recorded_at: now})
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
assert has_element?(view, "#filter-grain-115")
assert has_element?(view, "#filter-grain-147")
end
test "renders subsonic filter toggle", %{conn: conn, caliber: caliber, retailer: retailer} do
now = DateTime.truncate(DateTime.utc_now(), :second)
product = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: true})
snapshot_fixture(%{product: product, price_per_round_cents: 25, in_stock: true, recorded_at: now})
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
assert has_element?(view, "#filter-subsonic")
end
test "displays grain weight and brand in product table", %{conn: conn, caliber: caliber, retailer: retailer} do
now = DateTime.truncate(DateTime.utc_now(), :second)
product =
product_fixture(%{
caliber: caliber,
retailer: retailer,
grain_weight: 115,
brand: "Federal",
in_stock: true
})
snapshot_fixture(%{product: product, price_per_round_cents: 28, in_stock: true, recorded_at: now})
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
assert has_element?(view, "#products [id]")
end
test "filters by casing when clicked", %{conn: conn, caliber: caliber, retailer: retailer} do
now = DateTime.truncate(DateTime.utc_now(), :second)
brass =
product_fixture(%{caliber: caliber, retailer: retailer, casing: "brass", in_stock: true, title: "Brass Ammo"})
steel =
product_fixture(%{caliber: caliber, retailer: retailer, casing: "steel", in_stock: true, title: "Steel Ammo"})
snapshot_fixture(%{product: brass, price_per_round_cents: 28, in_stock: true, recorded_at: now})
snapshot_fixture(%{product: steel, price_per_round_cents: 20, in_stock: true, recorded_at: now})
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
# Click steel filter
view |> element("#filter-casing-steel") |> render_click()
# Should still have the filter button active
assert has_element?(view, "#filter-casing-steel")
end
test "receives real-time price updates via PubSub", %{conn: conn, caliber: caliber, retailer: retailer} do
now = DateTime.truncate(DateTime.utc_now(), :second)
product = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: true})
snapshot_fixture(%{product: product, price_per_round_cents: 30, in_stock: true, recorded_at: now})
{:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
# Add a new product + snapshot
new_product =
product_fixture(%{caliber: caliber, retailer: retailer, in_stock: true, title: "New Product", casing: "brass"})
snapshot_fixture(%{product: new_product, price_per_round_cents: 18, in_stock: true, recorded_at: now})
# Broadcast price update
Phoenix.PubSub.broadcast(Ammoprices.PubSub, "prices:updated", {:prices_updated, %{}})
# The view should re-render with updated data
html = render(view)
assert html =~ "New Product"
end
end
end