feat: implement standard-compliant sidebar with comprehensive tests
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
Implement a new sidebar component based on DaisyUI Drawer pattern without custom CSS variants. The sidebar supports desktop (expanded/collapsed states) and mobile (overlay drawer) with full accessibility compliance. Sidebar Implementation: - Refactor sidebar component with sidebar_header, menu_item, menu_group, sidebar_footer sub-components - Add logo (mila.svg) with size-8 (32px) always visible - Implement toggle button with icon swap (chevron-left/right) for desktop - Add nested menu support with details/summary (expanded) and dropdown (collapsed) patterns - Implement footer with language selector (expanded-only), theme toggle, and user menu with avatar - Update layouts.ex to use drawer pattern with data-sidebar-expanded attribute for state management CSS & JavaScript: - Add CSS styles for sidebar state management via data-attribute selectors - Implement SidebarState JavaScript hook for localStorage persistence - Add smooth width transitions (w-64 ↔ w-16) for desktop collapsed state - Add CSS classes for expanded-only, menu-label, and icon visibility Documentation: - Add sidebar-analysis-current-state.md: Analysis of current implementation - Add sidebar-requirements-v2.md: Complete specification for new sidebar - Add daisyui-drawer-pattern.md: DaisyUI pattern documentation - Add umsetzung-sidebar.md: Step-by-step implementation guide Testing: - Add comprehensive component tests for all sidebar sub-components - Add integration tests for sidebar state management and mobile drawer - Extend accessibility tests (ARIA labels, roles, keyboard navigation) - Add regression tests for duplicate IDs, hover effects, and tooltips - Ensure full test coverage per specification requirements
This commit is contained in:
parent
b0097ab99d
commit
16ca4efc03
10 changed files with 5439 additions and 194 deletions
850
test/mv_web/components/layouts/sidebar_test.exs
Normal file
850
test/mv_web/components/layouts/sidebar_test.exs
Normal file
|
|
@ -0,0 +1,850 @@
|
|||
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 links should not be rendered
|
||||
refute html =~ ~s(href="/members")
|
||||
refute html =~ ~s(href="/users")
|
||||
refute html =~ ~s(href="/settings")
|
||||
refute html =~ ~s(href="/contribution_types")
|
||||
|
||||
# Footer section should not be rendered
|
||||
refute html =~ "locale-select"
|
||||
refute html =~ "theme-controller"
|
||||
end
|
||||
|
||||
test "T2.3: renders menu items when current_user is present" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# Check for Members link
|
||||
assert html =~ ~s(href="/members")
|
||||
|
||||
# Check for Users link
|
||||
assert html =~ ~s(href="/users")
|
||||
|
||||
# Check for Custom Fields link
|
||||
assert html =~ ~s(href="/custom_fields")
|
||||
|
||||
# Check for Contributions section
|
||||
assert html =~ ~s(href="/contribution_types")
|
||||
assert html =~ ~s(href="/contribution_settings")
|
||||
|
||||
# Check for Settings link (placeholder)
|
||||
assert html =~ ~s(href="#")
|
||||
end
|
||||
|
||||
test "T2.4: renders sidebar with main-sidebar ID" do
|
||||
html = render_sidebar(authenticated_assigns(true))
|
||||
|
||||
assert html =~ ~s(id="main-sidebar")
|
||||
end
|
||||
|
||||
test "T2.5: renders sidebar with main-sidebar ID on desktop" do
|
||||
html = render_sidebar(authenticated_assigns(false))
|
||||
|
||||
assert html =~ ~s(id="main-sidebar")
|
||||
end
|
||||
end
|
||||
|
||||
# =============================================================================
|
||||
# Group 3: Menu Structure (T3.1-T3.3)
|
||||
# =============================================================================
|
||||
|
||||
describe "menu structure" do
|
||||
test "T3.1: renders flat menu items with icons and labels" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# Check for Members link with icon
|
||||
assert html =~ ~s(href="/members")
|
||||
assert html =~ "hero-users"
|
||||
|
||||
# Check for Users link with icon
|
||||
assert html =~ ~s(href="/users")
|
||||
assert html =~ "hero-user-circle"
|
||||
|
||||
# Check for Custom Fields link with icon
|
||||
assert html =~ ~s(href="/custom_fields")
|
||||
assert html =~ "hero-rectangle-group"
|
||||
|
||||
# Check for Settings link with icon
|
||||
assert html =~ ~s(href="#")
|
||||
assert html =~ "hero-cog-6-tooth"
|
||||
|
||||
# Check for tooltips (data-tip attribute)
|
||||
assert html =~ "data-tip="
|
||||
end
|
||||
|
||||
test "T3.2: renders nested menu with details element for expanded state" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# Check for Contributions section structure with details
|
||||
assert html =~ "<details"
|
||||
assert has_class?(html, "expanded-menu-group")
|
||||
|
||||
# Check for contribution links
|
||||
assert html =~ ~s(href="/contribution_types")
|
||||
assert html =~ ~s(href="/contribution_settings")
|
||||
end
|
||||
|
||||
test "T3.3: renders nested menu with dropdown for collapsed state" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# Check for collapsed dropdown container
|
||||
assert has_class?(html, "collapsed-menu-group")
|
||||
assert has_class?(html, "dropdown")
|
||||
assert has_class?(html, "dropdown-right")
|
||||
|
||||
# Check for dropdown-content
|
||||
assert has_class?(html, "dropdown-content")
|
||||
|
||||
# Check for icon button
|
||||
assert html =~ "hero-currency-dollar"
|
||||
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 for hero icons
|
||||
assert html =~ "hero-users"
|
||||
assert html =~ "hero-user-circle"
|
||||
assert html =~ "hero-rectangle-group"
|
||||
assert html =~ "hero-currency-dollar"
|
||||
assert html =~ "hero-cog-6-tooth"
|
||||
assert html =~ "hero-chevron-left"
|
||||
assert html =~ "hero-chevron-right"
|
||||
|
||||
# Icons should have aria-hidden
|
||||
assert html =~ ~s(aria-hidden="true")
|
||||
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"
|
||||
|
||||
# Navigation section
|
||||
assert html =~ ~s(role="menubar")
|
||||
|
||||
# Footer section
|
||||
assert html =~ "theme-controller"
|
||||
|
||||
# All expected links
|
||||
expected_links = [
|
||||
"/members",
|
||||
"/users",
|
||||
"/custom_fields",
|
||||
"/contribution_types",
|
||||
"/contribution_settings",
|
||||
"/sign-out"
|
||||
]
|
||||
|
||||
for link <- expected_links do
|
||||
assert html =~ ~s(href="#{link}"), "Missing link: #{link}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# =============================================================================
|
||||
# Component Tests - sidebar_header/1
|
||||
# =============================================================================
|
||||
|
||||
describe "sidebar_header/1" do
|
||||
test "renders logo with correct size" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# Logo is size-8 (32px)
|
||||
assert html =~ ~s(src="/images/mila.svg")
|
||||
assert html =~ ~s(alt="Mila Logo")
|
||||
assert has_class?(html, "size-8")
|
||||
|
||||
# Logo is always visible (no conditional classes)
|
||||
assert html =~ ~s(<img)
|
||||
end
|
||||
|
||||
test "renders club name" do
|
||||
assigns = %{
|
||||
current_user: %{id: "user-1", email: "test@example.com"},
|
||||
club_name: "My Test Club",
|
||||
mobile: false
|
||||
}
|
||||
|
||||
html = render_sidebar(assigns)
|
||||
|
||||
# Club name is present
|
||||
assert html =~ "My Test Club"
|
||||
assert has_class?(html, "menu-label")
|
||||
end
|
||||
|
||||
test "renders toggle button for desktop" do
|
||||
html = render_sidebar(authenticated_assigns(false))
|
||||
|
||||
# Toggle button has both icons
|
||||
assert has_class?(html, "sidebar-expanded-icon")
|
||||
assert has_class?(html, "sidebar-collapsed-icon")
|
||||
|
||||
# Toggle button is not mobile (hidden on mobile)
|
||||
assert html =~ ~s(id="sidebar-toggle")
|
||||
assert html =~ ~s(aria-label="Toggle sidebar")
|
||||
assert html =~ ~s(aria-expanded="true")
|
||||
end
|
||||
|
||||
test "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
|
||||
|
||||
# =============================================================================
|
||||
# Component Tests - menu_item/1
|
||||
# =============================================================================
|
||||
|
||||
describe "menu_item/1" do
|
||||
test "renders icon and label" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# Icon is visible
|
||||
assert html =~ "hero-users"
|
||||
assert html =~ ~s(aria-hidden="true")
|
||||
|
||||
# Label is present
|
||||
assert html =~ "Members"
|
||||
assert has_class?(html, "menu-label")
|
||||
end
|
||||
|
||||
test "has tooltip with correct text" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# data-tip attribute set
|
||||
assert html =~ ~s(data-tip="Members")
|
||||
assert has_class?(html, "tooltip")
|
||||
assert has_class?(html, "tooltip-right")
|
||||
end
|
||||
|
||||
test "has correct link" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# navigate attribute correct (rendered as href)
|
||||
assert html =~ ~s(href="/members")
|
||||
assert html =~ ~s(role="menuitem")
|
||||
end
|
||||
end
|
||||
|
||||
# =============================================================================
|
||||
# Component Tests - menu_group/1
|
||||
# =============================================================================
|
||||
|
||||
describe "menu_group/1" do
|
||||
test "renders expanded menu group" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# details/summary present
|
||||
assert html =~ "<details"
|
||||
assert html =~ "<summary"
|
||||
assert has_class?(html, "expanded-menu-group")
|
||||
end
|
||||
|
||||
test "renders collapsed menu group" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# dropdown present
|
||||
assert has_class?(html, "collapsed-menu-group")
|
||||
assert has_class?(html, "dropdown")
|
||||
assert has_class?(html, "dropdown-right")
|
||||
end
|
||||
|
||||
test "renders submenu items" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# Inner_block items rendered
|
||||
assert html =~ ~s(href="/contribution_types")
|
||||
assert html =~ ~s(href="/contribution_settings")
|
||||
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 =~ ~s(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(~s(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 =~ ~s(id="sidebar-toggle") do
|
||||
toggle_count = html |> String.split(~s(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 html =~ ~s(class="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 =~ "<details"
|
||||
end
|
||||
|
||||
test "tooltips only visible when collapsed" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# Tooltip classes are present (CSS handles visibility)
|
||||
assert has_class?(html, "tooltip")
|
||||
assert has_class?(html, "tooltip-right")
|
||||
assert html =~ "data-tip="
|
||||
end
|
||||
|
||||
test "user menu dropdown has correct structure" do
|
||||
html = render_sidebar(authenticated_assigns())
|
||||
|
||||
# Dropdown should have proper classes
|
||||
assert has_class?(html, "dropdown")
|
||||
assert has_class?(html, "dropdown-top")
|
||||
assert has_class?(html, "dropdown-content")
|
||||
|
||||
# Should have both Profile and Logout links
|
||||
assert html =~ ~s(href="/sign-out")
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue