defmodule MvWeb.Layouts.SidebarTest do @moduledoc """ Unit tests for the Sidebar component. Tests cover: - Basic rendering and structure - Props handling (current_user, club_name, mobile) - Menu structure (flat and nested items) - Footer/Profile section - Accessibility attributes (ARIA labels, roles) - CSS classes (DaisyUI conformance) - Icon rendering - Conditional visibility """ use MvWeb.ConnCase, async: true import Phoenix.LiveViewTest import MvWeb.Layouts.Sidebar # ============================================================================= # Helper Functions # ============================================================================= # Returns assigns for an authenticated user with all required attributes. defp authenticated_assigns(mobile \\ false) do %{ current_user: %{id: "user-123", email: "test@example.com"}, club_name: "Test Club", mobile: mobile } end # Returns assigns for a guest user (not authenticated). defp guest_assigns(mobile \\ false) do %{ current_user: nil, club_name: "Test Club", mobile: mobile } end # Renders the sidebar component with the given assigns. defp render_sidebar(assigns) do render_component(&sidebar/1, assigns) end # Checks if the HTML contains a specific CSS class. defp has_class?(html, class) do html =~ class end # ============================================================================= # Group 1: Basic Rendering (T1.1-T1.3) # ============================================================================= describe "basic rendering" do test "T1.1: renders sidebar with main navigation structure" do html = render_sidebar(authenticated_assigns()) # Check for navigation element with correct ID assert html =~ ~s(id="main-sidebar") assert html =~ ~s(aria-label="Main navigation") # Check for sidebar class assert has_class?(html, "sidebar") end test "T1.2: renders logo correctly" do html = render_sidebar(authenticated_assigns()) # Check for logo image assert html =~ ~s(src="/images/mila.svg") assert html =~ ~s(alt="Mila Logo") # Check logo has correct size class assert has_class?(html, "size-8") end test "T1.3: renders toggle button with correct attributes (desktop only)" do html = render_sidebar(authenticated_assigns(false)) # Check for toggle button assert html =~ ~s(id="sidebar-toggle") assert html =~ "onclick=" # Check for DaisyUI button classes assert has_class?(html, "btn") assert has_class?(html, "btn-ghost") assert has_class?(html, "btn-sm") assert has_class?(html, "btn-square") # Check for both toggle icons (expanded and collapsed) assert has_class?(html, "sidebar-expanded-icon") assert has_class?(html, "sidebar-collapsed-icon") end test "T1.4: does not render toggle button on mobile" do html = render_sidebar(authenticated_assigns(true)) # Toggle button should not be rendered on mobile refute html =~ ~s(id="sidebar-toggle") end end # ============================================================================= # Group 2: Props Handling (T2.1-T2.3) # ============================================================================= describe "props handling" do test "T2.1: displays club name when provided" do assigns = %{ current_user: %{id: "user-1", email: "test@example.com"}, club_name: "My Awesome Club", mobile: false } html = render_sidebar(assigns) assert html =~ "My Awesome Club" end test "T2.2: does not render menu items when current_user is nil" do html = render_sidebar(guest_assigns()) # Navigation menu should not be rendered refute html =~ ~s(role="menubar") refute html =~ ~s(role="menuitem") # Footer section should not be rendered refute html =~ "theme-controller" refute html =~ "locale-select" end test "T2.3: renders menu items when current_user is present" do html = render_sidebar(authenticated_assigns()) # Check that menu structure exists assert html =~ ~s(role="menubar") assert html =~ ~s(role="menuitem") # Check that top-level menu items exist (at least one) # Count menu items with tooltips (top-level items have tooltips) menu_item_count = html |> String.split("data-tip=") |> length() |> Kernel.-(1) assert menu_item_count > 0, "Should have at least one top-level menu item" # Check that nested menu groups exist assert html =~ " String.split(~s(role="menuitem")) |> length() |> Kernel.-(1) data_tip_count = html |> String.split("data-tip=") |> length() |> Kernel.-(1) # There should be more menuitems than data-tips (nested items don't have data-tip) assert menuitem_count > data_tip_count, "Should have nested menu items (menuitems without data-tip)" end test "T3.3: renders nested menu with dropdown for collapsed state" do html = render_sidebar(authenticated_assigns()) # Check for collapsed dropdown structure assert has_class?(html, "collapsed-menu-group") assert has_class?(html, "dropdown") assert has_class?(html, "dropdown-right") assert has_class?(html, "dropdown-content") # Check that dropdown button has icon (any hero icon) assert html =~ ~r/hero-\w+/ # Check ARIA attributes assert html =~ ~s(aria-haspopup="menu") end end # ============================================================================= # Group 4: Footer/Profile Section (T4.1-T4.3) # ============================================================================= describe "footer/profile section" do test "T4.1: renders footer section when user is authenticated" do html = render_sidebar(authenticated_assigns()) # Check for footer container with mt-auto assert has_class?(html, "mt-auto") # Check for language selector form assert html =~ ~s(action="/set_locale") # Check for theme toggle assert has_class?(html, "theme-controller") # Check for user menu/avatar assert has_class?(html, "avatar") end test "T4.2: renders language selector with form and options" do html = render_sidebar(authenticated_assigns()) # Check for form with correct action assert html =~ ~s(action="/set_locale") assert html =~ ~s(method="post") # Check for CSRF token assert html =~ "_csrf_token" # Check for select element assert html =~ ~s(name="locale") # Check for language options assert html =~ ~s(value="de") assert html =~ "Deutsch" assert html =~ ~s(value="en") assert html =~ "English" # Check expanded-only class assert has_class?(html, "expanded-only") end test "T4.3: renders user dropdown with profile and logout links" do assigns = %{ current_user: %{id: "user-456", email: "test@example.com"}, club_name: "Test Club", mobile: false } html = render_sidebar(assigns) # Check for dropdown container assert has_class?(html, "dropdown") assert has_class?(html, "dropdown-top") # Check for avatar button assert html =~ ~s(aria-haspopup="menu") # Check for profile link (with user ID) assert html =~ ~s(href="/users/user-456") # Check for logout link assert html =~ ~s(href="/sign-out") # Check for DaisyUI dropdown classes assert has_class?(html, "dropdown-content") end test "T4.4: renders user avatar with placeholder" do assigns = %{ current_user: %{id: "user-789", email: "alice@example.com"}, club_name: "Test Club", mobile: false } html = render_sidebar(assigns) # Should have avatar placeholder classes assert has_class?(html, "avatar") assert has_class?(html, "placeholder") assert has_class?(html, "bg-neutral") assert has_class?(html, "text-neutral-content") end end # ============================================================================= # Group 5: Accessibility Attributes (T5.1-T5.5) # ============================================================================= describe "accessibility attributes" do test "T5.1: navigation has correct ARIA label" do html = render_sidebar(authenticated_assigns()) assert html =~ ~s(aria-label="Main navigation") end test "T5.2: toggle button has correct ARIA attributes" do html = render_sidebar(authenticated_assigns(false)) # Toggle button assert html =~ ~s(aria-label="Toggle sidebar") assert html =~ ~s(aria-controls="main-sidebar") assert html =~ ~s(aria-expanded="true") end test "T5.3: menu has correct roles" do html = render_sidebar(authenticated_assigns()) # Main menu should have menubar role assert html =~ ~s(role="menubar") # List items should have role="none" assert html =~ ~s(role="none") # Links should have role="menuitem" assert html =~ ~s(role="menuitem") end test "T5.4: nested menu has correct ARIA attributes" do html = render_sidebar(authenticated_assigns()) # Details summary should have haspopup assert html =~ ~s(aria-haspopup="true") # Dropdown button should have haspopup assert html =~ ~s(aria-haspopup="menu") # Nested menus should have role="menu" assert html =~ ~s(role="menu") end test "T5.5: icons are hidden from screen readers" do html = render_sidebar(authenticated_assigns()) # Icons should have aria-hidden="true" assert html =~ ~s(aria-hidden="true") end end # ============================================================================= # Group 6: CSS Classes - DaisyUI Conformance (T6.1-T6.4) # ============================================================================= describe "CSS classes - DaisyUI conformance" do test "T6.1: uses correct DaisyUI menu classes" do html = render_sidebar(authenticated_assigns()) # menu class on ul assert has_class?(html, "menu") end test "T6.2: uses correct DaisyUI button classes" do html = render_sidebar(authenticated_assigns(false)) # Button classes assert has_class?(html, "btn") assert has_class?(html, "btn-ghost") assert has_class?(html, "btn-sm") assert has_class?(html, "btn-square") end test "T6.3: uses correct tooltip classes" do html = render_sidebar(authenticated_assigns()) # Tooltip classes for menu items assert has_class?(html, "tooltip") assert has_class?(html, "tooltip-right") end test "T6.4: uses correct dropdown classes" do html = render_sidebar(authenticated_assigns()) # Dropdown classes assert has_class?(html, "dropdown") assert has_class?(html, "dropdown-right") assert has_class?(html, "dropdown-top") assert has_class?(html, "dropdown-content") assert has_class?(html, "rounded-box") end end # ============================================================================= # Group 7: Icon Rendering (T7.1-T7.2) # ============================================================================= describe "icon rendering" do test "T7.1: renders hero icons for menu items" do html = render_sidebar(authenticated_assigns()) # Check that hero icons are present (pattern matching) assert html =~ ~r/hero-\w+/ # Check that icons have aria-hidden assert html =~ ~s(aria-hidden="true") # Check for specific structural icons (toggle, theme) that should always exist assert html =~ "hero-chevron-left" assert html =~ "hero-chevron-right" assert html =~ "hero-sun" assert html =~ "hero-moon" end test "T7.2: renders icons for theme toggle" do html = render_sidebar(authenticated_assigns()) # Theme toggle icons (sun and moon) assert html =~ "hero-sun" assert html =~ "hero-moon" end end # ============================================================================= # Group 8: State-dependent classes (T8.1-T8.3) # ============================================================================= describe "state-dependent classes" do test "T8.1: expanded-only elements have correct CSS class" do html = render_sidebar(authenticated_assigns()) # Language form should be expanded-only assert has_class?(html, "expanded-only") end test "T8.2: menu-label class is used for text that hides when collapsed" do html = render_sidebar(authenticated_assigns()) # Menu labels that hide in collapsed state assert has_class?(html, "menu-label") end test "T8.3: toggle button has state icons" do html = render_sidebar(authenticated_assigns(false)) # Expanded icon assert has_class?(html, "sidebar-expanded-icon") # Collapsed icon assert has_class?(html, "sidebar-collapsed-icon") end end # ============================================================================= # Additional Edge Cases and Validation # ============================================================================= describe "edge cases" do test "handles user with minimal attributes" do assigns = %{ current_user: %{id: "minimal-user", email: "min@test.com"}, club_name: "Minimal Club", mobile: false } html = render_sidebar(assigns) # Should render without error assert html =~ "Minimal Club" assert html =~ ~s(href="/users/minimal-user") end test "handles empty club name" do assigns = %{ current_user: %{id: "user-1", email: "test@test.com"}, club_name: "", mobile: false } html = render_sidebar(assigns) # Should render without error assert html =~ ~s(id="main-sidebar") end test "sidebar structure is complete with all sections" do html = render_sidebar(authenticated_assigns()) # Header section assert html =~ "Mila Logo" assert html =~ ~s(src="/images/mila.svg") # Navigation section assert html =~ ~s(role="menubar") assert html =~ ~s(id="main-sidebar") # Check that menu has items (at least one top-level item) assert html =~ ~s(role="menuitem") # Check that nested menus exist assert html =~ " String.split(~s(role="menuitem")) |> length() |> Kernel.-(1) data_tip_count = html |> String.split("data-tip=") |> length() |> Kernel.-(1) # There should be more menuitems than data-tips (nested items don't have data-tip) assert menuitem_count > data_tip_count, "Should have nested menu items (menuitems without data-tip)" # Verify nested menu structure exists assert html =~ ~s(role="menu") end end # ============================================================================= # Component Tests - sidebar_footer/1 # ============================================================================= describe "sidebar_footer/1" do test "renders at bottom of sidebar" do html = render_sidebar(authenticated_assigns()) # mt-auto present assert has_class?(html, "mt-auto") end test "renders theme toggle" do html = render_sidebar(authenticated_assigns()) # Toggle is always visible assert has_class?(html, "theme-controller") assert html =~ "hero-sun" assert html =~ "hero-moon" end test "renders language selector in expanded only" do html = render_sidebar(authenticated_assigns()) # expanded-only class assert has_class?(html, "expanded-only") assert html =~ ~s(action="/set_locale") end test "renders user menu with avatar" do assigns = %{ current_user: %{id: "user-123", email: "alice@example.com"}, club_name: "Test Club", mobile: false } html = render_sidebar(assigns) # Avatar present assert has_class?(html, "avatar") assert has_class?(html, "placeholder") # First letter correct (A for alice@example.com) assert html =~ "A" end end # ============================================================================= # Integration Tests - Sidebar State Management # ============================================================================= describe "sidebar state management" do test "sidebar starts expanded by default" do # The data-sidebar-expanded attribute is set in layouts.ex, not in sidebar.ex # This test verifies the sidebar structure supports the expanded state html = render_sidebar(authenticated_assigns()) # Sidebar has classes that support expanded state assert has_class?(html, "menu-label") assert has_class?(html, "expanded-only") end test "toggle button has correct onclick handler" do html = render_sidebar(authenticated_assigns(false)) # Toggle button has onclick handler assert html =~ ~r/onclick="toggleSidebar\(\)"/ end test "no duplicate IDs in layout" do html = render_sidebar(authenticated_assigns()) # Check that main-sidebar ID appears only once id_count = html |> String.split(~r/id="main-sidebar"/) |> length() |> Kernel.-(1) assert id_count == 1, "main-sidebar ID should appear exactly once" # Check that sidebar-toggle ID appears only once (if present) if html =~ ~r/id="sidebar-toggle"/ do toggle_count = html |> String.split(~r/id="sidebar-toggle"/) |> length() |> Kernel.-(1) assert toggle_count == 1, "sidebar-toggle ID should appear exactly once" end end end # ============================================================================= # Integration Tests - Mobile Drawer # ============================================================================= describe "mobile drawer" do test "mobile header renders on small screens" do # Mobile header is in layouts.ex, not sidebar.ex # This test verifies sidebar works correctly with mobile flag html = render_sidebar(authenticated_assigns(true)) # Sidebar should render without toggle button on mobile refute html =~ ~s(id="sidebar-toggle") end test "drawer overlay is present" do html = render_sidebar(authenticated_assigns()) # Drawer overlay label for mobile assert html =~ ~s(for="mobile-drawer") assert has_class?(html, "drawer-overlay") assert html =~ ~s(lg:hidden) end end # ============================================================================= # Accessibility Tests - Extended # ============================================================================= describe "accessibility - extended" do test "sidebar has aria-label" do html = render_sidebar(authenticated_assigns()) assert html =~ ~s(aria-label="Main navigation") end test "toggle button has aria-label and aria-expanded" do html = render_sidebar(authenticated_assigns(false)) # aria-label present assert html =~ ~s(aria-label="Toggle sidebar") # aria-expanded="true" or "false" assert html =~ ~s(aria-expanded="true") || html =~ ~s(aria-expanded="false") end test "menu items have role attributes" do html = render_sidebar(authenticated_assigns()) # role="menubar", "menuitem", "menu" assert html =~ ~s(role="menubar") assert html =~ ~s(role="menuitem") assert html =~ ~s(role="menu") end test "icons have aria-hidden" do html = render_sidebar(authenticated_assigns()) # Decorative icons: aria-hidden="true" # Count occurrences to ensure multiple icons have it assert html =~ ~s(aria-hidden="true") end test "user menu has aria-haspopup" do html = render_sidebar(authenticated_assigns()) # aria-haspopup="menu" assert html =~ ~s(aria-haspopup="menu") end end # ============================================================================= # Regression Tests # ============================================================================= describe "regression tests" do test "no duplicate profile links" do html = render_sidebar(authenticated_assigns()) # Only one Profile link in DOM profile_count = html |> String.split(~s[href="/users/"]) |> length() |> Kernel.-(1) assert profile_count <= 1, "Should have at most one profile link" end test "nested menu has only one hover effect" do html = render_sidebar(authenticated_assigns()) # Check that menu-group has proper structure # Both expanded and collapsed versions should be present assert has_class?(html, "expanded-menu-group") assert has_class?(html, "collapsed-menu-group") # Details element should not have duplicate hover classes # (CSS handles this, but we verify structure) assert html =~ "