Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
203 changes: 68 additions & 135 deletions frontend/__tests__/unit/pages/CreateModule.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMutation, useQuery, useApolloClient } from '@apollo/client/react'
import { addToast } from '@heroui/toast'
import { screen, fireEvent, waitFor, act } from '@testing-library/react'
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useRouter, useParams } from 'next/navigation'
import { useSession } from 'next-auth/react'
import { render } from 'wrappers/testUtil'
Expand Down Expand Up @@ -29,164 +29,97 @@ jest.mock('@apollo/client/react', () => ({
describe('CreateModulePage', () => {
const mockPush = jest.fn()
const mockReplace = jest.fn()
const mockCreateModule = jest.fn()

// Helper function to fill all required form fields
const fillRequiredFields = () => {
fireEvent.change(screen.getByLabelText('Name *'), { target: { value: 'Test Module' } })
fireEvent.change(screen.getByLabelText(/Description/i), {
target: { value: 'A test description' },
})
fireEvent.change(screen.getByLabelText(/Start Date/i), { target: { value: '2025-01-01' } })
fireEvent.change(screen.getByLabelText(/End Date/i), { target: { value: '2025-01-02' } })
fireEvent.change(screen.getByLabelText(/Project Name/i), { target: { value: 'Test' } })
}

// Helper to handle the asynchronous project selection and form submission
const selectProjectAndSubmit = async () => {
// Wait for the debounced search to trigger and for the suggestion to appear
const suggestionButton = await screen.findByRole('button', { name: /Awesome Project/i })
fireEvent.click(suggestionButton)

// Submit the form
fireEvent.click(screen.getByRole('button', { name: /Create Module/i }))
}
const mockQuery = jest.fn().mockResolvedValue({
data: {
searchProjects: [{ id: '123', name: 'Awesome Project' }],
},
})

beforeEach(() => {
// Reset mocks before each test to ensure isolation
jest.clearAllMocks()
;(useRouter as jest.Mock).mockReturnValue({ push: mockPush, replace: mockReplace })
;(useParams as jest.Mock).mockReturnValue({ programKey: 'test-program' })
;(useApolloClient as jest.Mock).mockReturnValue({
query: jest.fn().mockResolvedValue({
data: { searchProjects: [{ id: '123', name: 'Awesome Project' }] },
}),
query: mockQuery,
})
})

it('submits the form and updates cache correctly on success', async () => {
;(useSession as jest.Mock).mockReturnValue({
data: { user: { login: 'admin-user' } },
status: 'authenticated',
})
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: { getProgram: { admins: [{ login: 'admin-user' }] } },
loading: false,
})
const mockCache = {
readQuery: jest.fn().mockReturnValue({ getProgram: {}, getProgramModules: [] }),
writeQuery: jest.fn(),
}
const createModuleFn = jest.fn(async (options) => {
options.update(mockCache, { data: { createModule: { id: 'new-module' } } })
return { data: { createModule: { id: 'new-module' } } }
})
;(useMutation as unknown as jest.Mock).mockReturnValue([createModuleFn, { loading: false }])

render(<CreateModulePage />)
fillRequiredFields()
await selectProjectAndSubmit()

await waitFor(() => {
expect(mockCache.writeQuery).toHaveBeenCalled()
expect(mockPush).toHaveBeenCalledWith('/my/mentorship/programs/test-program')
})
afterEach(() => {
jest.clearAllMocks()
})

it('redirects non-admin users', async () => {
jest.useFakeTimers()
;(useSession as jest.Mock).mockReturnValue({
data: { user: { login: 'not-an-admin' } },
status: 'authenticated',
})
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: { getProgram: { admins: [{ login: 'admin-user' }] } },
loading: false,
})
it('submits the form and navigates to programs page', async () => {
const user = userEvent.setup()

render(<CreateModulePage />)

await waitFor(() => {
expect(addToast).toHaveBeenCalledWith(expect.objectContaining({ title: 'Access Denied' }))
})

// Advance timers by 1.5 seconds to trigger the redirect
act(() => {
jest.advanceTimersByTime(1500)
})

await waitFor(() => {
expect(mockReplace).toHaveBeenCalledWith('/my/mentorship')
})
jest.useRealTimers()
})

it('handles submission failure correctly', async () => {
;(useSession as jest.Mock).mockReturnValue({
data: { user: { login: 'admin-user' } },
status: 'authenticated',
})
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: { getProgram: { admins: [{ login: 'admin-user' }] } },
data: {
getProgram: {
admins: [{ login: 'admin-user' }],
},
},
loading: false,
})
const submissionError = new Error('Submission failed')
const createModuleFn = jest.fn().mockRejectedValue(submissionError)
;(useMutation as unknown as jest.Mock).mockReturnValue([createModuleFn, { loading: false }])
;(useMutation as unknown as jest.Mock).mockReturnValue([
mockCreateModule.mockResolvedValue({
data: {
createModule: {
key: 'my-test-module',
},
},
}),
{ loading: false },
])

render(<CreateModulePage />)
fillRequiredFields()
await selectProjectAndSubmit()

await waitFor(() => {
expect(addToast).toHaveBeenCalledWith(
expect.objectContaining({ description: submissionError.message })
)
})
})

it('handles cache updates when cache is empty or mutation data is missing', async () => {
;(useSession as jest.Mock).mockReturnValue({
data: { user: { login: 'admin-user' } },
status: 'authenticated',
})
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: { getProgram: { admins: [{ login: 'admin-user' }] } },
loading: false,
})
const mockCache = { readQuery: jest.fn(), writeQuery: jest.fn() }
const createModuleFn = jest.fn()
;(useMutation as unknown as jest.Mock).mockReturnValue([createModuleFn, { loading: false }])

const { rerender } = render(<CreateModulePage />)

fillRequiredFields()
mockCache.readQuery.mockReturnValue(null)
createModuleFn.mockImplementation(async (options) => {
options.update(mockCache, { data: { createModule: { id: 'new-module' } } })
return {}
})

await selectProjectAndSubmit()
await waitFor(() => {
expect(mockCache.writeQuery).not.toHaveBeenCalled()
expect(mockPush).toHaveBeenCalledTimes(1)
})

rerender(<CreateModulePage />)
fillRequiredFields()
mockCache.writeQuery.mockClear()
mockPush.mockClear()
// Fill all inputs
await user.type(screen.getByLabelText('Name'), 'My Test Module')
await user.type(screen.getByLabelText(/Description/i), 'This is a test module')
await user.type(screen.getByLabelText(/Start Date/i), '2025-07-15')
await user.type(screen.getByLabelText(/End Date/i), '2025-08-15')
await user.type(screen.getByLabelText(/Domains/i), 'AI, ML')
await user.type(screen.getByLabelText(/Tags/i), 'react, graphql')

const projectInput = await waitFor(() => {
return screen.getByPlaceholderText('Start typing project name...')
})

await user.type(projectInput, 'Aw')

await waitFor(
() => {
expect(mockQuery).toHaveBeenCalled()
},
{ timeout: 2000 }
)

const projectOption = await waitFor(
() => {
return (
screen.queryByRole('option', { name: /Awesome Project/i }) ||
screen.queryByText('Awesome Project') ||
document.querySelector('[data-key="123"]')
)
},
{ timeout: 2000 }
)

if (projectOption) {
await user.click(projectOption)
} else {
await user.type(projectInput, '{ArrowDown}{Enter}')
}

mockCache.readQuery.mockReturnValue({ getProgram: {}, getProgramModules: [] })
createModuleFn.mockImplementation(async (options) => {
options.update(mockCache, { data: { createModule: null } })
return {}
})
await user.click(screen.getByRole('button', { name: /Create Module/i }))

await selectProjectAndSubmit()
await waitFor(() => {
expect(mockCache.writeQuery).not.toHaveBeenCalled()
expect(mockPush).toHaveBeenCalledTimes(1)
expect(mockCreateModule).toHaveBeenCalled()
expect(mockPush).toHaveBeenCalledWith('/my/mentorship/programs/test-program')
})
})
})
35 changes: 23 additions & 12 deletions frontend/__tests__/unit/pages/CreateProgram.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMutation } from '@apollo/client/react'
import { useMutation, useApolloClient } from '@apollo/client/react'
import { addToast } from '@heroui/toast'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { useRouter as mockUseRouter } from 'next/navigation'
Expand All @@ -9,6 +9,7 @@ import CreateProgramPage from 'app/my/mentorship/programs/create/page'
jest.mock('@apollo/client/react', () => ({
...jest.requireActual('@apollo/client/react'),
useMutation: jest.fn(),
useApolloClient: jest.fn(),
}))

jest.mock('next/navigation', () => ({
Expand All @@ -29,12 +30,22 @@ jest.mock('@heroui/toast', () => ({

const mockRouterPush = jest.fn()
const mockCreateProgram = jest.fn()
const mockQuery = jest.fn().mockResolvedValue({
data: {
myPrograms: {
programs: [],
},
},
})

describe('CreateProgramPage (comprehensive tests)', () => {
beforeEach(() => {
jest.clearAllMocks()
;(mockUseRouter as jest.Mock).mockReturnValue({ push: mockRouterPush })
;(useMutation as unknown as jest.Mock).mockReturnValue([mockCreateProgram, { loading: false }])
;(useApolloClient as jest.Mock).mockReturnValue({
query: mockQuery,
})
})

test('redirects if unauthenticated', async () => {
Expand All @@ -60,7 +71,7 @@ describe('CreateProgramPage (comprehensive tests)', () => {

render(<CreateProgramPage />)

expect(screen.queryByLabelText('Name *')).not.toBeInTheDocument()
expect(screen.queryByLabelText('Name')).not.toBeInTheDocument()
})

test('redirects with toast if not a project leader', async () => {
Expand Down Expand Up @@ -103,7 +114,7 @@ describe('CreateProgramPage (comprehensive tests)', () => {

render(<CreateProgramPage />)

expect(await screen.findByLabelText('Name *')).toBeInTheDocument()
expect(await screen.findByLabelText('Name')).toBeInTheDocument()
})

test('submits form and redirects on success', async () => {
Expand All @@ -127,16 +138,16 @@ describe('CreateProgramPage (comprehensive tests)', () => {

render(<CreateProgramPage />)

fireEvent.change(screen.getByLabelText('Name *'), {
fireEvent.change(screen.getByLabelText('Name'), {
target: { value: 'Test Program' },
})
fireEvent.change(screen.getByLabelText('Description *'), {
fireEvent.change(screen.getByLabelText(/^Description/), {
target: { value: 'A description' },
})
fireEvent.change(screen.getByLabelText('Start Date *'), {
fireEvent.change(screen.getByLabelText('Start Date'), {
target: { value: '2025-01-01' },
})
fireEvent.change(screen.getByLabelText('End Date *'), {
fireEvent.change(screen.getByLabelText('End Date'), {
target: { value: '2025-12-31' },
})
fireEvent.change(screen.getByLabelText('Tags'), {
Expand All @@ -154,7 +165,7 @@ describe('CreateProgramPage (comprehensive tests)', () => {
input: {
name: 'Test Program',
description: 'A description',
menteesLimit: 5,
menteesLimit: 0,
startedAt: '2025-01-01',
endedAt: '2025-12-31',
tags: ['tag1', 'tag2'],
Expand Down Expand Up @@ -186,16 +197,16 @@ describe('CreateProgramPage (comprehensive tests)', () => {

render(<CreateProgramPage />)

fireEvent.change(screen.getByLabelText('Name *'), {
fireEvent.change(screen.getByLabelText('Name'), {
target: { value: 'Test Program' },
})
fireEvent.change(screen.getByLabelText('Description *'), {
fireEvent.change(screen.getByLabelText(/^Description/), {
target: { value: 'A description' },
})
fireEvent.change(screen.getByLabelText('Start Date *'), {
fireEvent.change(screen.getByLabelText('Start Date'), {
target: { value: '2025-01-01' },
})
fireEvent.change(screen.getByLabelText('End Date *'), {
fireEvent.change(screen.getByLabelText('End Date'), {
target: { value: '2025-12-31' },
})

Expand Down
5 changes: 3 additions & 2 deletions frontend/__tests__/unit/pages/EditModule.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe('EditModulePage', () => {
expect(await screen.findByDisplayValue('Existing Module')).toBeInTheDocument()

// Modify values
fireEvent.change(screen.getByLabelText('Name *'), {
fireEvent.change(screen.getByLabelText('Name'), {
target: { value: 'Updated Name' },
})
fireEvent.change(screen.getByLabelText(/Description/i), {
Expand All @@ -103,7 +103,8 @@ describe('EditModulePage', () => {
fireEvent.change(screen.getByLabelText(/Tags/i), {
target: { value: 'graphql, react' },
})
fireEvent.change(screen.getByLabelText(/Project Name/i), {
const projectInput = screen.getByPlaceholderText(/Start typing project name/i)
fireEvent.change(projectInput, {
target: { value: 'Awesome Project' },
})

Expand Down
Loading