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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const renderPage = new RenderPage();
describe('Verify page credential rendering', () => {
function verifyErrorDisplayed(errorText: string) {
cy.contains(errorText, { timeout: 10000 }).should('be.visible');
cy.get('button').contains('JSON').should('not.exist');
cy.contains('button', 'JSON').should('not.exist');
}

describe('successful verification', () => {
Expand Down
125 changes: 50 additions & 75 deletions packages/reference-implementation/src/app/(protected)/layout.test.tsx
Original file line number Diff line number Diff line change
@@ -1,126 +1,101 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ProtectedLayout from './layout';

const mockPush = jest.fn();
const mockPathname = jest.fn(() => '/configuration/dids');

jest.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
}),
usePathname: () => mockPathname(),
usePathname: () => '/dashboard',
}));

jest.mock('lucide-react', () => ({
LogOut: () => <span>LogOut</span>,
}));

const mockUseAuth = jest.fn();

jest.mock('@/contexts/auth', () => ({
AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
useAuth: () => ({
user: { name: 'Test User', email: 'test@example.com', roles: [] },
isLoading: false,
isAuthenticated: true,
logout: jest.fn(),
}),
useAuth: () => mockUseAuth(),
}));

jest.mock('@/contexts/did/DidContext', () => ({
DidProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));

// Mocked so the absence assertions trip if the layout renders the sidebars again.
// The mocks render their test ids unconditionally (the real Sidebar swaps to a
// skeleton without its test id while loading) and avoid the real components'
// dependencies on '@reference-implementation/components', mocked below. See #715.
jest.mock('@/components/sidebar', () => ({
Sidebar: ({ onNavClick, selectedNavId }: { onNavClick: (id: string) => void; selectedNavId?: string }) => (
<div data-testid='sidebar' data-selected-nav-id={selectedNavId}>
<button data-testid='nav-dids' onClick={() => onNavClick('dids')}>
DIDs
</button>
<button data-testid='nav-credentials' onClick={() => onNavClick('credentials')}>
Credentials
</button>
<button data-testid='nav-resources' onClick={() => onNavClick('resources')}>
Resources
</button>
</div>
),
Sidebar: () => <div data-testid='sidebar' />,
MobileSidebar: () => <div data-testid='mobile-sidebar' />,
}));

jest.mock('@reference-implementation/components', () => ({
Loader: () => <div data-testid='loader' />,
}));

jest.mock('lucide-react', () => ({
LogOut: () => <span>LogOut</span>,
}));

describe('ProtectedLayout navigation', () => {
describe('ProtectedLayout', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseAuth.mockReturnValue({
user: { name: 'Test User', email: 'test@example.com', roles: [] },
isLoading: false,
isAuthenticated: true,
logout: jest.fn(),
});
});

it('calls router.push when clicking a mapped nav item', async () => {
const user = userEvent.setup();
it('renders children without the navigation sidebars', () => {
render(
<ProtectedLayout>
<div>Content</div>
<div data-testid='content'>Content</div>
</ProtectedLayout>,
);

await user.click(screen.getByTestId('nav-dids'));

expect(mockPush).toHaveBeenCalledWith('/configuration/dids');
expect(screen.getByTestId('content')).toBeInTheDocument();
expect(screen.queryByTestId('sidebar')).toBeNull();
expect(screen.queryByTestId('mobile-sidebar')).toBeNull();
});

it('does not call router.push when clicking an unmapped nav item', async () => {
const user = userEvent.setup();
render(
<ProtectedLayout>
<div>Content</div>
</ProtectedLayout>,
);

await user.click(screen.getByTestId('nav-credentials'));
it('shows the loader while authentication is pending', () => {
mockUseAuth.mockReturnValue({
user: null,
isLoading: true,
isAuthenticated: false,
logout: jest.fn(),
});

expect(mockPush).not.toHaveBeenCalled();
});

it('does not call router.push for external nav items', async () => {
const user = userEvent.setup();
render(
<ProtectedLayout>
<div>Content</div>
<div data-testid='content'>Content</div>
</ProtectedLayout>,
);

await user.click(screen.getByTestId('nav-resources'));

expect(screen.getByTestId('loader')).toBeInTheDocument();
expect(screen.queryByTestId('content')).toBeNull();
expect(mockPush).not.toHaveBeenCalled();
});

it('derives selectedNavId from the current pathname', () => {
mockPathname.mockReturnValue('/configuration/dids');
render(
<ProtectedLayout>
<div>Content</div>
</ProtectedLayout>,
);

expect(screen.getByTestId('sidebar')).toHaveAttribute('data-selected-nav-id', 'dids');
});

it('sets selectedNavId to undefined for unrecognised paths', () => {
mockPathname.mockReturnValue('/dashboard');
render(
<ProtectedLayout>
<div>Content</div>
</ProtectedLayout>,
);

expect(screen.getByTestId('sidebar')).not.toHaveAttribute('data-selected-nav-id', 'dids');
});
it('redirects to sign-in when unauthenticated', () => {
mockUseAuth.mockReturnValue({
user: null,
isLoading: false,
isAuthenticated: false,
logout: jest.fn(),
});

it('matches sub-paths to the correct nav item', () => {
mockPathname.mockReturnValue('/configuration/dids/create');
render(
<ProtectedLayout>
<div>Content</div>
<div data-testid='content'>Content</div>
</ProtectedLayout>,
);

expect(screen.getByTestId('sidebar')).toHaveAttribute('data-selected-nav-id', 'dids');
expect(mockPush).toHaveBeenCalledWith('/api/auth/signin');
expect(screen.getByTestId('loader')).toBeInTheDocument();
expect(screen.queryByTestId('content')).toBeNull();
});
});
29 changes: 18 additions & 11 deletions packages/reference-implementation/src/app/(protected)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { DidProvider } from '@/contexts/did/DidContext';
import { Sidebar, MobileSidebar } from '@/components/sidebar';
import { LogOut } from 'lucide-react';

// Navigation is hidden until the pages it links to exist; flip to true to reinstate (#715).
const SHOW_NAVIGATION = false;

interface ProtectedLayoutProps {
children: React.ReactNode;
}
Expand Down Expand Up @@ -144,17 +147,21 @@ function ProtectedContent({ children }: { children: React.ReactNode }) {

return (
<div className='flex h-screen overflow-hidden'>
{/* Mobile Sidebar/Navbar - hidden on desktop */}
<div className='md:hidden'>
<MobileSidebar {...sidebarProps} />
</div>

{/* Desktop Sidebar - hidden on mobile */}
<div className='hidden md:block'>
<Sidebar {...sidebarProps} />
</div>

<main className='flex-1 overflow-auto pt-16 md:pt-0'>
{SHOW_NAVIGATION && (
<>
{/* Mobile Sidebar/Navbar - hidden on desktop */}
<div className='md:hidden'>
<MobileSidebar {...sidebarProps} />
</div>

{/* Desktop Sidebar - hidden on mobile */}
<div className='hidden md:block'>
<Sidebar {...sidebarProps} />
</div>
</>
)}

<main className={`flex-1 overflow-auto${SHOW_NAVIGATION ? ' pt-16 md:pt-0' : ''}`}>
<div className='px-6 py-6'>
{isLoading ? (
<Loader size={60} text='Loading...' className='min-h-[calc(100vh-3rem)]' />
Expand Down
36 changes: 36 additions & 0 deletions packages/reference-implementation/src/app/(public)/layout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { render, screen } from '@testing-library/react';
import PublicLayout from './layout';

// Mocked with a root test id so the absence assertion trips if the layout
// renders the Header again (the real Header has no test id on its root). See #715.
jest.mock('@/components/Header/Header', () => ({
__esModule: true,
default: () => <div data-testid='header' />,
}));

jest.mock('@reference-implementation/components', () => ({
Footer: () => <div data-testid='footer' />,
}));

describe('PublicLayout', () => {
it('renders children without the header', () => {
render(
<PublicLayout>
<div data-testid='content'>Content</div>
</PublicLayout>,
);

expect(screen.getByTestId('content')).toBeInTheDocument();
expect(screen.queryByTestId('header')).toBeNull();
});

it('renders the footer', () => {
render(
<PublicLayout>
<div>Content</div>
</PublicLayout>,
);

expect(screen.getByTestId('footer')).toBeInTheDocument();
});
});
7 changes: 5 additions & 2 deletions packages/reference-implementation/src/app/(public)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import { Container } from '@mui/material';

import Header from '../../components/Header/Header';

// Navigation is hidden until the pages it links to exist; flip to true to reinstate (#715).
const SHOW_NAVIGATION = false;

export default function PublicLayout({ children }: { children: React.ReactNode }) {
return (
<Container
sx={{
mt: '64px',
mt: SHOW_NAVIGATION ? '64px' : '24px',
mb: '24px',
}}
>
<Header />
{SHOW_NAVIGATION && <Header />}
{children}
<Footer />
</Container>
Expand Down
Loading