Skip to content
Merged
24 changes: 12 additions & 12 deletions docker-compose/local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ services:
- 8000:8000
volumes:
- ../backend:/home/owasp
- backend-venv:/home/owasp/.venv
- backend-venv-mentorship:/home/owasp/.venv

cache:
command: >
Expand All @@ -48,7 +48,7 @@ services:
networks:
- nest-network
volumes:
- cache-data:/data
- cache-data-mentorship:/data

db:
container_name: nest-db
Expand All @@ -65,7 +65,7 @@ services:
networks:
- nest-network
volumes:
- db-data:/var/lib/postgresql/data
- db-data-mentorship:/var/lib/postgresql/data

docs:
container_name: nest-docs
Expand All @@ -82,7 +82,7 @@ services:
- 8001:8001
volumes:
- ../docs:/home/owasp/docs
- docs-venv:/home/owasp/.venv
- docs-venv-mentorship:/home/owasp/.venv

frontend:
container_name: nest-frontend
Expand All @@ -104,16 +104,16 @@ services:
- 3000:3000
volumes:
- ../frontend:/home/owasp
- frontend-next:/home/owasp/.next
- frontend-node-modules:/home/owasp/node_modules
- frontend-next-mentorship:/home/owasp/.next
- frontend-node-modules-mentorship:/home/owasp/node_modules

networks:
nest-network:

volumes:
backend-venv:
cache-data:
db-data:
docs-venv:
frontend-next:
frontend-node-modules:
backend-venv-mentorship:
cache-data-mentorship:
db-data-mentorship:
docs-venv-mentorship:
frontend-next-mentorship:
frontend-node-modules-mentorship:
26 changes: 21 additions & 5 deletions frontend/__tests__/unit/components/ProgramCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ jest.mock('components/ActionButton', () => ({
),
}))

jest.mock('@heroui/tooltip', () => ({
Tooltip: ({ children, content }: { children: React.ReactNode; content: string }) => (
<div data-testid="tooltip" title={content}>
{children}
</div>
),
}))

describe('ProgramCard', () => {
const mockOnEdit = jest.fn()
const mockOnView = jest.fn()
Expand Down Expand Up @@ -178,23 +186,31 @@ describe('ProgramCard', () => {
})

describe('Description Handling', () => {
it('truncates long descriptions to 100 characters', () => {
const longDescription = 'A'.repeat(150)
it('renders long descriptions with line-clamp-6 CSS class', () => {
const longDescription = 'A'.repeat(300) // Long enough to trigger line clamping
const longDescProgram = { ...baseMockProgram, description: longDescription }

render(<ProgramCard program={longDescProgram} onView={mockOnView} accessLevel="user" />)

const expectedText = 'A'.repeat(100) + '...'
expect(screen.getByText(expectedText)).toBeInTheDocument()
// Check that the full description is rendered (CSS handles the visual truncation)
expect(screen.getByText(longDescription)).toBeInTheDocument()

// Check that the paragraph has the line-clamp-6 class
const descriptionElement = screen.getByText(longDescription)
expect(descriptionElement).toHaveClass('line-clamp-6')
})

it('shows full description when under 100 characters', () => {
it('shows full description when short', () => {
const shortDescription = 'Short description'
const shortDescProgram = { ...baseMockProgram, description: shortDescription }

render(<ProgramCard program={shortDescProgram} onView={mockOnView} accessLevel="user" />)

expect(screen.getByText('Short description')).toBeInTheDocument()

// Check that it still has line-clamp-6 class for consistency
const descriptionElement = screen.getByText('Short description')
expect(descriptionElement).toHaveClass('line-clamp-6')
})

it('shows fallback text when description is empty', () => {
Expand Down
213 changes: 31 additions & 182 deletions frontend/__tests__/unit/components/SingleModuleCard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { faUsers } from '@fortawesome/free-solid-svg-icons'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { screen } from '@testing-library/react'
import { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'
import React from 'react'
import { render } from 'wrappers/testUtil'
import type { ExtendedSession } from 'types/auth'
import type { Module } from 'types/mentorship'
import { ExperienceLevelEnum, ProgramStatusEnum } from 'types/mentorship'
import SingleModuleCard from 'components/SingleModuleCard'
Expand Down Expand Up @@ -111,16 +110,6 @@ const mockModule: Module = {

const mockAdmins = [{ login: 'admin1' }, { login: 'admin2' }]

const mockSessionData: ExtendedSession = {
user: {
login: 'admin1',
isLeader: true,
email: '[email protected]',
image: 'https://example.com/admin-avatar.jpg',
},
expires: '2024-12-31T23:59:59Z',
}

describe('SingleModuleCard', () => {
beforeEach(() => {
jest.clearAllMocks()
Expand All @@ -146,7 +135,6 @@ describe('SingleModuleCard', () => {
expect(screen.getByText('Test Module')).toBeInTheDocument()
expect(screen.getByText('This is a test module description')).toBeInTheDocument()
expect(screen.getByTestId('icon-users')).toBeInTheDocument()
expect(screen.getByTestId('icon-ellipsis')).toBeInTheDocument()
})

it('renders module details correctly', () => {
Expand Down Expand Up @@ -183,128 +171,31 @@ describe('SingleModuleCard', () => {
})
})

describe('Dropdown Menu', () => {
it('opens dropdown when ellipsis button is clicked', () => {
render(<SingleModuleCard module={mockModule} />)

const ellipsisButton = screen.getByRole('button')
fireEvent.click(ellipsisButton)

expect(screen.getByText('View Module')).toBeInTheDocument()
})

it('closes dropdown when clicking outside', async () => {
render(<SingleModuleCard module={mockModule} />)

const ellipsisButton = screen.getByRole('button')
fireEvent.click(ellipsisButton)

expect(screen.getByText('View Module')).toBeInTheDocument()

// Click outside the dropdown
fireEvent.mouseDown(document.body)

await waitFor(() => {
expect(screen.queryByText('View Module')).not.toBeInTheDocument()
})
})

it('navigates to view module when View Module is clicked', () => {
describe('Simplified Interface', () => {
it('focuses on content display only', () => {
render(<SingleModuleCard module={mockModule} />)

const ellipsisButton = screen.getByRole('button')
fireEvent.click(ellipsisButton)

const viewButton = screen.getByText('View Module')
fireEvent.click(viewButton)

expect(mockPush).toHaveBeenCalledWith('//modules/test-module')
})

it('shows only View Module option for non-admin users', () => {
render(
<SingleModuleCard
module={mockModule}
showEdit={true}
accessLevel="user"
admins={mockAdmins}
/>
)

const ellipsisButton = screen.getByRole('button')
fireEvent.click(ellipsisButton)
// Should display core content
expect(screen.getByText('Test Module')).toBeInTheDocument()
expect(screen.getByText('This is a test module description')).toBeInTheDocument()
expect(screen.getByText('Experience Level:')).toBeInTheDocument()

expect(screen.getByText('View Module')).toBeInTheDocument()
expect(screen.queryByText('Edit Module')).not.toBeInTheDocument()
expect(screen.queryByText('Create Module')).not.toBeInTheDocument()
// Should have clickable title for navigation
const moduleLink = screen.getByTestId('module-link')
expect(moduleLink).toHaveAttribute('href', '//modules/test-module')
})
})

describe('Admin Functionality', () => {
beforeEach(() => {
mockUseSession.mockReturnValue({
data: mockSessionData,
status: 'authenticated',
update: jest.fn(),
})
})

it('shows Edit Module option for admin users when showEdit is true', () => {
render(
<SingleModuleCard
module={mockModule}
showEdit={true}
accessLevel="admin"
admins={mockAdmins}
/>
)

const ellipsisButton = screen.getByRole('button')
fireEvent.click(ellipsisButton)

expect(screen.getByText('View Module')).toBeInTheDocument()
expect(screen.getByText('Edit Module')).toBeInTheDocument()
expect(screen.getByText('Create Module')).toBeInTheDocument()
})

it('does not show Edit Module option when showEdit is false', () => {
render(
<SingleModuleCard
module={mockModule}
showEdit={false}
accessLevel="admin"
admins={mockAdmins}
/>
)

const ellipsisButton = screen.getByRole('button')
fireEvent.click(ellipsisButton)

expect(screen.getByText('View Module')).toBeInTheDocument()
expect(screen.queryByText('Edit Module')).not.toBeInTheDocument()
expect(screen.getByText('Create Module')).toBeInTheDocument()
})

it('navigates to edit module when Edit Module is clicked', () => {
render(
<SingleModuleCard
module={mockModule}
showEdit={true}
accessLevel="admin"
admins={mockAdmins}
/>
)

const ellipsisButton = screen.getByRole('button')
fireEvent.click(ellipsisButton)

const editButton = screen.getByText('Edit Module')
fireEvent.click(editButton)
describe('Props Handling', () => {
it('renders correctly with minimal props', () => {
render(<SingleModuleCard module={mockModule} />)

expect(mockPush).toHaveBeenCalledWith('//modules/test-module/edit')
expect(screen.getByText('Test Module')).toBeInTheDocument()
expect(screen.getByText('This is a test module description')).toBeInTheDocument()
})

it('navigates to create module when Create Module is clicked', () => {
it('ignores admin-related props since menu is removed', () => {
// These props are now ignored but should not cause errors
render(
<SingleModuleCard
module={mockModule}
Expand All @@ -314,13 +205,7 @@ describe('SingleModuleCard', () => {
/>
)

const ellipsisButton = screen.getByRole('button')
fireEvent.click(ellipsisButton)

const createButton = screen.getByText('Create Module')
fireEvent.click(createButton)

expect(mockPush).toHaveBeenCalledWith('//modules/create')
expect(screen.getByText('Test Module')).toBeInTheDocument()
})
})

Expand All @@ -338,67 +223,31 @@ describe('SingleModuleCard', () => {
expect(screen.queryByTestId('top-contributors-list')).not.toBeInTheDocument()
})

it('handles undefined admins array', () => {
it('handles undefined admins array gracefully', () => {
render(<SingleModuleCard module={mockModule} showEdit={true} accessLevel="admin" />)

const ellipsisButton = screen.getByRole('button')
fireEvent.click(ellipsisButton)

expect(screen.getByText('View Module')).toBeInTheDocument()
expect(screen.queryByText('Edit Module')).not.toBeInTheDocument()
})

it('handles null session data', () => {
mockUseSession.mockReturnValue({
data: null,
status: 'unauthenticated',
update: jest.fn(),
})

render(
<SingleModuleCard
module={mockModule}
showEdit={true}
accessLevel="admin"
admins={mockAdmins}
/>
)

const ellipsisButton = screen.getByRole('button')
fireEvent.click(ellipsisButton)

expect(screen.getByText('View Module')).toBeInTheDocument()
expect(screen.queryByText('Edit Module')).not.toBeInTheDocument()
// Should render without errors even with admin props
expect(screen.getByText('Test Module')).toBeInTheDocument()
})
})

describe('Accessibility', () => {
it('has proper button roles and interactions', () => {
it('has accessible link for module navigation', () => {
render(<SingleModuleCard module={mockModule} />)

const ellipsisButton = screen.getByRole('button')
expect(ellipsisButton).toBeInTheDocument()

fireEvent.click(ellipsisButton)

const viewButton = screen.getByText('View Module')
expect(viewButton.closest('button')).toBeInTheDocument()
const moduleLink = screen.getByTestId('module-link')
expect(moduleLink).toBeInTheDocument()
expect(moduleLink).toHaveAttribute('href', '//modules/test-module')
expect(moduleLink).toHaveAttribute('target', '_blank')
expect(moduleLink).toHaveAttribute('rel', 'noopener noreferrer')
})

it('supports keyboard navigation', () => {
it('has proper heading structure', () => {
render(<SingleModuleCard module={mockModule} />)

const ellipsisButton = screen.getByRole('button')

// Focus the button
ellipsisButton.focus()
expect(ellipsisButton).toHaveFocus()

// Press Enter to open dropdown
fireEvent.keyDown(ellipsisButton, { key: 'Enter', code: 'Enter' })
fireEvent.click(ellipsisButton) // Simulate the click that would happen

expect(screen.getByText('View Module')).toBeInTheDocument()
const moduleTitle = screen.getByRole('heading', { level: 1 })
expect(moduleTitle).toBeInTheDocument()
expect(moduleTitle).toHaveTextContent('Test Module')
})
})

Expand Down
2 changes: 1 addition & 1 deletion frontend/__tests__/unit/pages/CreateModule.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ describe('CreateModulePage', () => {

await waitFor(() => {
expect(mockCreateModule).toHaveBeenCalled()
expect(mockPush).toHaveBeenCalledWith('/my/mentorship/programs/test-program?refresh=true')
expect(mockPush).toHaveBeenCalledWith('/my/mentorship/programs/test-program')
})
})
})
Loading