Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions x-pack/solutions/security/packages/side-nav/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
* 2.0.
*/

export {
SolutionSideNav,
type SolutionSideNavProps,
type SolutionSideNavInteractionVariant,
} from './src';
export { SolutionSideNav, type SolutionSideNavProps } from './src';
export { SolutionSideNavItemPosition } from './src/types';
export type { SolutionSideNavItem, Tracker } from './src/types';
4 changes: 2 additions & 2 deletions x-pack/solutions/security/packages/side-nav/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import type { SolutionSideNavInteractionVariant, SolutionSideNavProps } from './solution_side_nav';
import type { SolutionSideNavProps } from './solution_side_nav';

export type { SolutionSideNavProps, SolutionSideNavInteractionVariant };
export type { SolutionSideNavProps };

const SolutionSideNavLazy = lazy(() => import('./solution_side_nav'));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import React from 'react';
import { render, screen } from '@testing-library/react';
import { render } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { SolutionSideNav } from './solution_side_nav';
import type { SolutionSideNavProps } from './solution_side_nav';
Expand Down Expand Up @@ -35,6 +35,19 @@ const mockItems: SolutionSideNavItem[] = [
label: 'Alerts',
href: '/alerts',
},
{
id: 'rulesLanding',
label: 'Rules',
href: '/rules',
items: [
{
id: 'rulesManagement',
label: 'Rules Management',
href: '/rules-management',
description: 'Rules Management description',
},
],
},
];

const renderNav = (props: Partial<SolutionSideNavProps> = {}) =>
Expand All @@ -58,7 +71,7 @@ describe('SolutionSideNav', () => {
const result = renderNav();
expect(
result.getByTestId(`solutionSideNavItemLink-${'dashboardsLanding'}`).getAttribute('href')
).toBe('/dashboards');
).toBe('/overview');
expect(result.getByTestId(`solutionSideNavItemLink-${'alerts'}`).getAttribute('href')).toBe(
'/alerts'
);
Expand All @@ -82,7 +95,16 @@ describe('SolutionSideNav', () => {
expect(mockOnClick).toHaveBeenCalled();
});

it('should send telemetry if link clicked', async () => {
it('renders right arrow hint when item has children', () => {
const result = renderNav({
items: mockItems,
selectedId: 'rules',
});

expect(result.getByTestId('solutionSideNavItemPanelHint-rulesLanding')).toBeInTheDocument();
});

it('should send telemetry when a link without children is clicked', async () => {
const items = [
...mockItems,
{
Expand All @@ -98,51 +120,41 @@ describe('SolutionSideNav', () => {
`${TELEMETRY_EVENT.NAVIGATION}${'exploreLanding'}`
);
});
});

describe('panel button toggle', () => {
it('should render the panel button only for nav items', () => {
const result = renderNav();
expect(
result.getByTestId(`solutionSideNavItemButton-${'dashboardsLanding'}`)
).toBeInTheDocument();
expect(result.queryByTestId(`solutionSideNavItemButton-${'alerts'}`)).not.toBeInTheDocument();
});

it('should render the panel when button is clicked', async () => {
it('should send telemetry when a parent link is clicked', async () => {
const result = renderNav();
expect(result.queryByTestId('solutionSideNavPanel')).not.toBeInTheDocument();

await userEvent.click(result.getByTestId(`solutionSideNavItemButton-${'dashboardsLanding'}`));
expect(result.getByTestId('solutionSideNavPanel')).toBeInTheDocument();
expect(result.getByText('Overview')).toBeInTheDocument();
await userEvent.click(result.getByTestId(`solutionSideNavItemLink-${'dashboardsLanding'}`));
expect(mockTrack).toHaveBeenCalledWith(
METRIC_TYPE.CLICK,
`${TELEMETRY_EVENT.PANEL_NAVIGATION_TOGGLE}${'dashboardsLanding'}`
);
});

it('should telemetry when button is clicked', async () => {
it('should render the sub-nav panel when link is clicked', async () => {
const result = renderNav();
expect(result.queryByTestId('solutionSideNavPanel')).not.toBeInTheDocument();

await userEvent.click(result.getByTestId(`solutionSideNavItemButton-${'dashboardsLanding'}`));
expect(mockTrack).toHaveBeenCalledWith(
METRIC_TYPE.CLICK,
`${TELEMETRY_EVENT.PANEL_NAVIGATION_TOGGLE}${'dashboardsLanding'}`
);
await userEvent.click(result.getByTestId(`solutionSideNavItemLink-${'dashboardsLanding'}`));
expect(result.getByTestId('solutionSideNavPanel')).toBeInTheDocument();
expect(result.getByText('Overview')).toBeInTheDocument();
});

it('should close the panel when the same button is clicked', async () => {
it('should close the sub-nav panel when the same link is clicked', async () => {
const result = renderNav();
await userEvent.click(result.getByTestId(`solutionSideNavItemButton-${'dashboardsLanding'}`));
await userEvent.click(result.getByTestId(`solutionSideNavItemLink-${'dashboardsLanding'}`));
expect(result.getByTestId('solutionSideNavPanel')).toBeInTheDocument();

await userEvent.click(result.getByTestId(`solutionSideNavItemButton-${'dashboardsLanding'}`));
await userEvent.click(result.getByTestId(`solutionSideNavItemLink-${'dashboardsLanding'}`));

// add check at the end of the event loop to ensure the panel is removed
setTimeout(() => {
expect(result.queryByTestId('solutionSideNavPanel')).not.toBeInTheDocument();
});
});

it('should open other panel when other button is clicked while open', async () => {
it('should open relevant sub-nav panel when another link is clicked while sub-nav is open', async () => {
const items = [
...mockItems,
{
Expand All @@ -161,186 +173,13 @@ describe('SolutionSideNav', () => {
];
const result = renderNav({ items });

await userEvent.click(result.getByTestId(`solutionSideNavItemButton-${'dashboardsLanding'}`));
await userEvent.click(result.getByTestId(`solutionSideNavItemLink-${'dashboardsLanding'}`));
expect(result.getByTestId('solutionSideNavPanel')).toBeInTheDocument();
expect(result.getByText('Overview')).toBeInTheDocument();

await userEvent.click(result.getByTestId(`solutionSideNavItemButton-${'exploreLanding'}`));
await userEvent.click(result.getByTestId(`solutionSideNavItemLink-${'exploreLanding'}`));
expect(result.queryByTestId('solutionSideNavPanel')).toBeInTheDocument();
expect(result.getByText('Users')).toBeInTheDocument();
});
});

describe('unifiedRow interaction variant', () => {
const firstChildOnClick = jest.fn((ev: { preventDefault: () => void }) => {
ev.preventDefault();
});
const panelItems: SolutionSideNavItem[] = [
{
id: 'dashboardsLanding',
label: 'Dashboards',
href: '/dashboards',
items: [
{
id: 'overview',
label: 'Overview',
href: '/overview-first',
onClick: firstChildOnClick,
description: 'Overview description',
},
],
},
{
id: 'alerts',
label: 'Alerts',
href: '/alerts',
},
{
id: 'Rules',
label: 'Rules',
href: '/rules',
items: [
{
id: 'rulesManagement',
label: 'Rules Management',
href: '/rules-management',
description: 'Rules Management description',
},
],
},
];

beforeEach(() => {
firstChildOnClick.mockClear();
});

it('renders arrow hint instead of split panel button when item has children', () => {
const result = renderNav({
items: panelItems,
navLinkInteractionVariant: 'unifiedRow',
selectedId: 'alerts',
});
expect(
result.queryByTestId('solutionSideNavItemButton-dashboardsLanding')
).not.toBeInTheDocument();
expect(
result.getByTestId('solutionSideNavItemPanelHint-dashboardsLanding')
).toBeInTheDocument();
});

it('uses first child href on the parent row link', () => {
const result = renderNav({
items: panelItems,
navLinkInteractionVariant: 'unifiedRow',
selectedId: 'alerts',
});
expect(
result.getByTestId('solutionSideNavItemLink-dashboardsLanding').getAttribute('href')
).toBe('/overview-first');
});

it('clicking the parent row opens the panel', async () => {
const result = renderNav({
items: panelItems,
navLinkInteractionVariant: 'unifiedRow',
selectedId: 'alerts',
});
await userEvent.click(result.getByTestId('solutionSideNavItemLink-dashboardsLanding'));
expect(result.getByTestId('solutionSideNavPanel')).toBeInTheDocument();
expect(result.getByText('Overview')).toBeInTheDocument();
expect(mockTrack).toHaveBeenCalledWith(
METRIC_TYPE.CLICK,
`${TELEMETRY_EVENT.PANEL_NAVIGATION_TOGGLE}dashboardsLanding`
);
});

it('should not show panel button in unifiedRow variant', () => {
const result = renderNav({
items: panelItems,
navLinkInteractionVariant: 'unifiedRow',
selectedId: 'alerts',
});
// Panel button should not exist for items with children in unifiedRow mode
expect(
result.queryByTestId('solutionSideNavItemButton-dashboardsLanding')
).not.toBeInTheDocument();
expect(result.queryByTestId('solutionSideNavItemButton-Rules')).not.toBeInTheDocument();
});

it('should navigate directly when clicking item without children in unifiedRow mode', async () => {
const mockOnClick = jest.fn((ev) => {
ev.preventDefault();
});
const itemsWithoutChildren: SolutionSideNavItem[] = [
{
id: 'alerts',
label: 'Alerts',
href: '/alerts',
onClick: mockOnClick,
},
];
const result = renderNav({
items: itemsWithoutChildren,
navLinkInteractionVariant: 'unifiedRow',
selectedId: 'alerts',
});
await userEvent.click(result.getByTestId('solutionSideNavItemLink-alerts'));
expect(mockOnClick).toHaveBeenCalled();
});
});

describe('navLinkInteractionVariant attribute', () => {
it('should use splitButton as default variant', () => {
const result = renderNav();
// In splitButton mode, panel button should be visible for items with children
expect(result.getByTestId('solutionSideNavItemButton-dashboardsLanding')).toBeInTheDocument();
});

it('should apply unifiedRow variant correctly', () => {
const result = renderNav({
items: mockItems,
navLinkInteractionVariant: 'unifiedRow',
selectedId: 'alerts',
});
// In unifiedRow mode, arrow hint should show instead of button
expect(
result.getByTestId('solutionSideNavItemPanelHint-dashboardsLanding')
).toBeInTheDocument();
expect(
result.queryByTestId('solutionSideNavItemButton-dashboardsLanding')
).not.toBeInTheDocument();
});

it('should switch between variants correctly', () => {
const { rerender } = render(
<SolutionSideNav
items={mockItems}
selectedId={'alerts'}
navLinkInteractionVariant="splitButton"
tracker={mockTrack}
/>
);

// Check splitButton mode
expect(screen.getByTestId('solutionSideNavItemButton-dashboardsLanding')).toBeInTheDocument();

// Re-render with unifiedRow
rerender(
<SolutionSideNav
items={mockItems}
selectedId={'alerts'}
navLinkInteractionVariant="unifiedRow"
tracker={mockTrack}
/>
);

// Check unifiedRow mode
expect(
screen.getByTestId('solutionSideNavItemPanelHint-dashboardsLanding')
).toBeInTheDocument();
expect(
screen.queryByTestId('solutionSideNavItemButton-dashboardsLanding')
).not.toBeInTheDocument();
});
});
});
Loading
Loading