add admin media library with image management and block picker integration
- Schema: alt, caption, tags fields on images table with metadata changeset - Context: list_images with filters, find_usages, used_image_ids, delete_with_cleanup - Admin UI: /admin/media with grid view, upload, filters, detail panel, metadata editing - Block editor: :image field type for image_text and content_body blocks - Page renderer: image_id resolution with legacy URL fallback - Mobile: bottom sheet detail panel with slide-up animation - CSS: uses correct --t-* admin theme tokens, admin-badge colour variants Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
154
test/berrypod_web/live/admin/media_test.exs
Normal file
154
test/berrypod_web/live/admin/media_test.exs
Normal file
@@ -0,0 +1,154 @@
|
||||
defmodule BerrypodWeb.Admin.MediaTest do
|
||||
use BerrypodWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Berrypod.AccountsFixtures
|
||||
|
||||
alias Berrypod.Media
|
||||
|
||||
@svg_attrs %{
|
||||
image_type: "logo",
|
||||
filename: "logo.svg",
|
||||
content_type: "image/svg+xml",
|
||||
file_size: 512,
|
||||
data: "<svg xmlns=\"http://www.w3.org/2000/svg\"><circle r=\"10\"/></svg>"
|
||||
}
|
||||
|
||||
@raster_attrs %{
|
||||
image_type: "media",
|
||||
filename: "banner.png",
|
||||
content_type: "image/png",
|
||||
file_size: 1024,
|
||||
data: <<137, 80, 78, 71>>,
|
||||
alt: "A banner image"
|
||||
}
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
%{user: user}
|
||||
end
|
||||
|
||||
describe "media library page" do
|
||||
setup %{conn: conn, user: user} do
|
||||
%{conn: log_in_user(conn, user)}
|
||||
end
|
||||
|
||||
test "renders media library for admin", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/media")
|
||||
assert html =~ "Media"
|
||||
assert html =~ "Upload image"
|
||||
end
|
||||
|
||||
test "shows images in grid", %{conn: conn} do
|
||||
{:ok, _} = Media.upload_image(@svg_attrs)
|
||||
{:ok, _} = Media.upload_image(@raster_attrs)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/media")
|
||||
assert html =~ "logo.svg"
|
||||
assert html =~ "banner.png"
|
||||
end
|
||||
|
||||
test "filters by type", %{conn: conn} do
|
||||
{:ok, _} = Media.upload_image(@svg_attrs)
|
||||
{:ok, _} = Media.upload_image(@raster_attrs)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/media")
|
||||
|
||||
html = render_change(view, "filter_type", %{"type" => "media"})
|
||||
assert html =~ "banner.png"
|
||||
refute html =~ "logo.svg"
|
||||
end
|
||||
|
||||
test "filters by search", %{conn: conn} do
|
||||
{:ok, _} = Media.upload_image(@svg_attrs)
|
||||
{:ok, _} = Media.upload_image(@raster_attrs)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/media")
|
||||
|
||||
html = render_keyup(view, "filter_search", %{"value" => "banner"})
|
||||
assert html =~ "banner.png"
|
||||
refute html =~ "logo.svg"
|
||||
end
|
||||
|
||||
test "selects image and shows detail panel", %{conn: conn} do
|
||||
{:ok, image} = Media.upload_image(@raster_attrs)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/media")
|
||||
|
||||
html = render_click(view, "select_image", %{"id" => image.id})
|
||||
assert html =~ "Image details"
|
||||
assert html =~ "banner.png"
|
||||
assert html =~ "Alt text"
|
||||
end
|
||||
|
||||
test "updates metadata", %{conn: conn} do
|
||||
{:ok, image} = Media.upload_image(@raster_attrs)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/media")
|
||||
render_click(view, "select_image", %{"id" => image.id})
|
||||
|
||||
render_submit(view, "update_metadata", %{
|
||||
"metadata" => %{"alt" => "Updated alt", "caption" => "New caption", "tags" => "hero"}
|
||||
})
|
||||
|
||||
updated = Media.get_image(image.id)
|
||||
assert updated.alt == "Updated alt"
|
||||
assert updated.caption == "New caption"
|
||||
assert updated.tags == "hero"
|
||||
end
|
||||
|
||||
test "deletes orphaned image", %{conn: conn} do
|
||||
{:ok, image} = Media.upload_image(@raster_attrs)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/media")
|
||||
render_click(view, "select_image", %{"id" => image.id})
|
||||
render_click(view, "confirm_delete")
|
||||
|
||||
html = render_click(view, "delete_image")
|
||||
assert html =~ "Image deleted"
|
||||
assert Media.get_image(image.id) == nil
|
||||
end
|
||||
|
||||
test "refuses to delete in-use image", %{conn: conn} do
|
||||
{:ok, image} = Media.upload_image(@svg_attrs)
|
||||
Berrypod.Settings.update_theme_settings(%{logo_image_id: image.id})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/media")
|
||||
render_click(view, "select_image", %{"id" => image.id})
|
||||
render_click(view, "confirm_delete")
|
||||
|
||||
html = render_click(view, "delete_image")
|
||||
assert html =~ "Cannot delete"
|
||||
assert Media.get_image(image.id) != nil
|
||||
end
|
||||
|
||||
test "orphan filter shows only unreferenced images", %{conn: conn} do
|
||||
{:ok, logo} = Media.upload_image(@svg_attrs)
|
||||
{:ok, _orphan} = Media.upload_image(@raster_attrs)
|
||||
Berrypod.Settings.update_theme_settings(%{logo_image_id: logo.id})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/media")
|
||||
|
||||
html = render_click(view, "toggle_orphans")
|
||||
assert html =~ "banner.png"
|
||||
refute html =~ "logo.svg"
|
||||
end
|
||||
|
||||
test "shows usage info for referenced image", %{conn: conn} do
|
||||
{:ok, image} = Media.upload_image(@svg_attrs)
|
||||
Berrypod.Settings.update_theme_settings(%{logo_image_id: image.id})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/media")
|
||||
html = render_click(view, "select_image", %{"id" => image.id})
|
||||
assert html =~ "Used in"
|
||||
assert html =~ "Logo"
|
||||
end
|
||||
|
||||
test "shows no-alt-text warning for images without alt", %{conn: conn} do
|
||||
{:ok, _} = Media.upload_image(Map.delete(@raster_attrs, :alt))
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/media")
|
||||
assert html =~ "No alt text"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user