/* global describe, it, expect, vi */
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';
import { SettingsPopover } from './SettingsPopover';
import type { SettingsPopoverAction } from './types';
// ── Mock icon ─────────────────────────────────────────────────────────────────
const MockIcon = () =>
React.createElement('svg', { 'data-testid': 'action-icon' });
// ── Fixtures ──────────────────────────────────────────────────────────────────
const MOCK_ACTIONS: SettingsPopoverAction[] = [
{
key: 'logout',
label: 'Logout',
icon: React.createElement(MockIcon),
onClick: vi.fn(),
},
];
const defaultProps = {
userName: 'Will Streeter',
userTier: 'Free Trial',
userEmail: 'w.streeter+002@tastymakers.io',
actions: MOCK_ACTIONS,
};
// ── Tests ─────────────────────────────────────────────────────────────────────
describe('SettingsPopover', () => {
// ── Trigger Rendering ────────────────────────────────────────────────────
describe('Trigger', () => {
it('renders a trigger button with the provided aria-label', () => {
render(
,
);
expect(
screen.getByRole('button', { name: 'Account settings' }),
).toBeInTheDocument();
});
it('defaults aria-label to "User settings"', () => {
render();
expect(
screen.getByRole('button', { name: 'User settings' }),
).toBeInTheDocument();
});
it('displays the user name on the trigger', () => {
render();
expect(screen.getByText('Will Streeter')).toBeInTheDocument();
});
it('displays the user tier on the trigger', () => {
render();
expect(screen.getByText('Free Trial')).toBeInTheDocument();
});
});
// ── Popover Open / Close ─────────────────────────────────────────────────
describe('Popover Toggle', () => {
it('popover content is not visible initially', () => {
render();
expect(
screen.queryByText(defaultProps.userEmail),
).not.toBeInTheDocument();
});
it('clicking the trigger opens the popover and shows the email', async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByRole('button', { name: 'User settings' }));
await waitFor(() => {
expect(screen.getByText(defaultProps.userEmail)).toBeInTheDocument();
});
});
it('clicking the trigger again closes the popover', async () => {
const user = userEvent.setup();
render();
const trigger = screen.getByRole('button', { name: 'User settings' });
await user.click(trigger);
await waitFor(() => {
expect(screen.getByText(defaultProps.userEmail)).toBeInTheDocument();
});
await user.click(trigger);
await waitFor(() => {
expect(
screen.queryByText(defaultProps.userEmail),
).not.toBeInTheDocument();
});
});
});
// ── Popover Content ──────────────────────────────────────────────────────
describe('Popover Content', () => {
it('displays the user email in the popover card', async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByRole('button', { name: 'User settings' }));
await waitFor(() => {
expect(screen.getByText(defaultProps.userEmail)).toBeInTheDocument();
});
});
it('renders all action items', async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByRole('button', { name: 'User settings' }));
await waitFor(() => {
expect(screen.getByText('Logout')).toBeInTheDocument();
});
});
it('renders action icons when provided', async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByRole('button', { name: 'User settings' }));
await waitFor(() => {
expect(screen.getByTestId('action-icon')).toBeInTheDocument();
});
});
it('renders actions without icons when icon is not provided', async () => {
const user = userEvent.setup();
const actionsNoIcon: SettingsPopoverAction[] = [
{ key: 'logout', label: 'Logout', onClick: vi.fn() },
];
render();
await user.click(screen.getByRole('button', { name: 'User settings' }));
await waitFor(() => {
expect(screen.getByText('Logout')).toBeInTheDocument();
});
});
});
// ── Actions ──────────────────────────────────────────────────────────────
describe('Actions', () => {
it('clicking an action calls its onClick handler', async () => {
const user = userEvent.setup();
const onLogout = vi.fn();
const actions: SettingsPopoverAction[] = [
{ key: 'logout', label: 'Logout', onClick: onLogout },
];
render();
await user.click(screen.getByRole('button', { name: 'User settings' }));
await waitFor(() => {
expect(screen.getByText('Logout')).toBeInTheDocument();
});
await user.click(screen.getByText('Logout'));
expect(onLogout).toHaveBeenCalledTimes(1);
});
it('renders multiple actions', async () => {
const user = userEvent.setup();
const actions: SettingsPopoverAction[] = [
{ key: 'logout', label: 'Logout', onClick: vi.fn() },
{ key: 'settings', label: 'Settings', onClick: vi.fn() },
];
render();
await user.click(screen.getByRole('button', { name: 'User settings' }));
await waitFor(() => {
expect(screen.getByText('Logout')).toBeInTheDocument();
expect(screen.getByText('Settings')).toBeInTheDocument();
});
});
});
// ── Avatar ───────────────────────────────────────────────────────────────
describe('Avatar', () => {
it('renders an avatar element in the trigger', () => {
const { container } = render();
// Avatar.Root renders a span with data-scope="avatar"
expect(
container.querySelector('[data-scope="avatar"]'),
).toBeInTheDocument();
});
it('uses custom avatarFallback when provided', () => {
render(
,
);
expect(screen.getByTestId('custom-fallback')).toBeInTheDocument();
});
});
// ── Accessibility ────────────────────────────────────────────────────────
describe('Accessibility', () => {
it('passes axe audit in closed state', async () => {
const { container } = render();
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('passes axe audit in open state', async () => {
const user = userEvent.setup();
const { container } = render();
await user.click(screen.getByRole('button', { name: 'User settings' }));
await waitFor(() => {
expect(screen.getByText(defaultProps.userEmail)).toBeInTheDocument();
});
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('trigger is keyboard-focusable', () => {
render();
const trigger = screen.getByRole('button', { name: 'User settings' });
expect(trigger.tagName).toBe('BUTTON');
});
it('action buttons inside the popover are keyboard-focusable', async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByRole('button', { name: 'User settings' }));
await waitFor(() => {
const logoutButton = screen.getByText('Logout').closest('button');
expect(logoutButton).toBeInTheDocument();
expect(logoutButton?.tagName).toBe('BUTTON');
});
});
});
});