Phoenix 1.8 app that scrapes ammunition retailers (Lucky Gunner, SGAmmo) for price data and displays historical price trends. - Data model: retailers, calibers, products, price snapshots - Scraper infrastructure with Req, Floki, realistic browser headers - Oban-scheduled scrape jobs (every 4h with randomized delays) - LiveView pages: homepage with category cards, caliber detail with price table, Chart.js price history, and price stats banner - 18 seeded calibers across handgun/rifle/rimfire/shotgun categories - 77 tests
93 lines
4 KiB
Elixir
93 lines
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
|
|
end
|
|
end
|