Skip to content

Comments

improve frontend test coverage#4033

Open
HarshitVerma109 wants to merge 5 commits intoOWASP:mainfrom
HarshitVerma109:feat/improve-frontend-test-coverage
Open

improve frontend test coverage#4033
HarshitVerma109 wants to merge 5 commits intoOWASP:mainfrom
HarshitVerma109:feat/improve-frontend-test-coverage

Conversation

@HarshitVerma109
Copy link
Contributor

@HarshitVerma109 HarshitVerma109 commented Feb 21, 2026

Proposed change

Resolves #3959
Improved frontend test coverage to 99.91%

Checklist

  • Required: I followed the contributing workflow
  • Required: I verified that my code works as intended and resolves the issue as described
  • Required: I ran make check-test locally: all warnings addressed, tests passed
  • I used AI for code, documentation, tests, or communication related to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Summary by CodeRabbit

Release Notes

  • New Features

    • Added breadcrumb context class registration utility for enhanced customization.
  • Bug Fixes

    • Improved error handling and messaging in module editing workflows.
    • Enhanced robustness when handling missing or invalid data across components.
    • Fixed zoom control behavior on map reactivation.
    • Corrected label filter logic for empty selection values.
  • Tests

    • Expanded test coverage for edge cases, null handling, and error scenarios across multiple components and pages.

Walkthrough

Adds many unit tests for null/undefined/edge cases across frontend components and pages; adjusts runtime control flow in several components (removed early-return guards, switched falsy/nullish checks, added non-null assertions), changes ChapterMap zoom-control replacement and makes SocialLinks non-exported.

Changes

Cohort / File(s) Summary
Component tests
frontend/__tests__/unit/components/CardDetailsPage.test.tsx, frontend/__tests__/unit/components/ChapterMap.test.tsx, frontend/__tests__/unit/components/ModuleForm.test.tsx, frontend/__tests__/unit/components/EntityActions.test.tsx
Added/expanded tests for empty/undefined languages/topics, leaders fallback, ChapterMap zoom-control lifecycle, ProjectSelector clear behavior and timer-based debounces, entity deletion cache-edge cases and non-Error error handling.
Context tests
frontend/__tests__/unit/contexts/BreadcrumbContext.test.tsx
Added tests for new exported utility registerBreadcrumbClassName including unregister return value and idempotence.
Page tests — error & auth flows
frontend/__tests__/unit/pages/EditModule.test.tsx, frontend/__tests__/unit/pages/EditProgram.test.tsx
Replaced spinner assertions with mocked ErrorDisplay/access-denied expectations; added tests for null-admins transition and permission-denied mutation behavior showing toasts.
Page tests — data edge cases
frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx, frontend/__tests__/unit/pages/SnapshotDetails.test.tsx, frontend/__tests__/unit/pages/UserDetails.test.tsx
Added tests covering invalid status fallback, undefined domains/tags, missing release id/repoName rendering, createdAt null fallback, avatar alt/title fallbacks, and minimal contribution heatmap rendering.
Page tests — interactions & params
frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx, frontend/__tests__/unit/pages/MyMentorship.test.tsx
Added tests ensuring no mutations when issueId missing, datepicker not opened when editing disallowed, and router push is not called when search params are unchanged.
Component implementations
frontend/src/components/CardDetailsPage.tsx, frontend/src/components/ChapterMap.tsx, frontend/src/components/ModuleForm.tsx, frontend/src/components/ProgramForm.tsx
Changed SocialLinks to internal (no export) and pass-through behavior; simplified milestone avatar alt fallback; ChapterMap now always replaces zoom control on activation; ProjectSelector now treats null as clear and calls parent with null, ''; uniqueness check no longer short-circuits on empty names.
Page implementations (forms & issues)
frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx, .../modules/[moduleKey]/edit/page.tsx, .../issues/[issueId]/page.tsx, .../issues/page.tsx
Switched status defaulting from `

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • arkid15r
  • kasya
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'improve frontend test coverage' clearly summarizes the main objective of the changeset, which focuses on adding comprehensive test coverage across multiple frontend components and pages.
Description check ✅ Passed The description is directly related to the changeset, referencing issue #3959, stating coverage improvements to 99.91%, and confirming all checklist items completed including local test validation.
Linked Issues check ✅ Passed The PR successfully addresses issue #3959 by adding comprehensive test coverage to the specified files: CardDetailsPage, ChapterMap, ModuleForm, BreadcrumbContext, EditModule, EditProgram, MenteeProfilePage, ModuleIssueDetailsPage, MyMentorship, SnapshotDetails, UserDetails, and EntityActions, meeting the 99%+ coverage objective.
Out of Scope Changes check ✅ Passed All code changes are directly in scope: test files add coverage for listed files, and minor production code changes (falsy to nullish checks, guard removals, export modifications, control flow simplifications) directly support the test coverage improvements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
frontend/__tests__/unit/pages/EditModule.test.tsx (1)

188-188: ⚠️ Potential issue | 🟡 Minor

Stale test name — update to match the actual assertion.

The description 'shows loading spinner when query returns an error' is a leftover from before the error-path refactor; the test body no longer checks for a spinner and instead asserts 'Access Denied'.

🐛 Proposed fix
- it('shows loading spinner when query returns an error', async () => {
+ it('shows Access Denied when query returns an error', async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/EditModule.test.tsx` at line 188, The test case
name is stale: the it(...) titled 'shows loading spinner when query returns an
error' no longer matches the assertion (which checks for 'Access Denied');
update the test description string to reflect the actual expectation (e.g.,
'shows Access Denied when query returns an error' or similar) so the test name
matches the assertion in EditModule.test.tsx (locate the it(...) block around
the assertion that looks for 'Access Denied').
frontend/__tests__/unit/components/ModuleForm.test.tsx (4)

657-687: ⚠️ Potential issue | 🟡 Minor

Test name is misleading — the Set-based selection path is never verified

After clicking autocomplete-select-item (which dispatches onSelectionChange(new Set(['project-1']))), the only assertion is:

await waitFor(() => {
  expect(mockOnProjectChange).toHaveBeenCalledWith(null, 'Test')
})

This only confirms the earlier handleInputChange side-effect (typing "Test"). The actual purpose of the test — verifying that handleSelectionChange extracts the ID from the Set and calls onProjectChange('project-1', 'Test Project 1') — is never asserted. Add the missing expectation:

🛠️ Proposed fix
-      // Verify that typing triggers onProjectChange with the search term
-      await waitFor(() => {
-        expect(mockOnProjectChange).toHaveBeenCalledWith(null, 'Test')
-      })
+      // Verify that typing triggers onProjectChange with the search term
+      await waitFor(() => {
+        expect(mockOnProjectChange).toHaveBeenCalledWith(null, 'Test')
+      })
+      // Verify that Set-based selection resolves to the correct project
+      expect(mockOnProjectChange).toHaveBeenCalledWith('project-1', 'Test Project 1')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx` around lines 657 -
687, The test currently only asserts the input change side-effect and never
verifies the Set-based selection path; update the test around the click on the
element with test id "autocomplete-select-item" (which triggers
onSelectionChange(new Set(['project-1'])) and is handled by
handleSelectionChange) to also assert that mockOnProjectChange was called with
the selected project ID and name
(expect(mockOnProjectChange).toHaveBeenCalledWith('project-1', 'Test Project
1')), using waitFor after the click to account for async handling.

657-687: ⚠️ Potential issue | 🟡 Minor

"handles selection via Set" only asserts the typing side-effect, not the Set selection itself

After clicking autocomplete-select-item (which invokes onSelectionChange(new Set(['project-1']))), the sole assertion is:

expect(mockOnProjectChange).toHaveBeenCalledWith(null, 'Test')

This only confirms the earlier handleInputChange call from typing "Test". The actual handleSelectionChange path for a Set — which should ultimately call onProjectChange('project-1', 'Test Project 1') — is never verified. Add the missing expectation after the waitFor:

🛠️ Proposed fix
       await waitFor(() => {
         expect(mockOnProjectChange).toHaveBeenCalledWith(null, 'Test')
       })
+
+      // Verify the Set-based selection resolved to the correct project
+      expect(mockOnProjectChange).toHaveBeenCalledWith('project-1', 'Test Project 1')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx` around lines 657 -
687, The test "handles selection via Set" only asserts the typing side-effect
and misses verifying the Set-selection path: after clicking the element with
test id "autocomplete-select-item" (which triggers onSelectionChange(new
Set(['project-1']))) add an assertion that mockOnProjectChange was called for
the selection, e.g.
expect(mockOnProjectChange).toHaveBeenCalledWith('project-1', 'Test Project 1');
place this check after the act/click and any waitFor so it validates the
handleSelectionChange -> onProjectChange flow invoked by the selection; keep the
existing typing assertions (renderProjectSelector, mockQuery, input change)
intact.

40-85: ⚠️ Potential issue | 🔴 Critical

Correct the mock signature — onSelectionChange should accept only React.Key, not Set, 'all', or null

HeroUI's Autocomplete onSelectionChange callback has the signature (key: React.Key) => void — it accepts a single key only, not null, Set, or 'all'.

The mock incorrectly declares:

onSelectionChange?: (key: React.Key | Set<React.Key> | 'all') => void

This type includes Set<React.Key> and 'all', which belong to the multi-select Selection type used by Select and DropdownMenu, not Autocomplete. Moreover, the mock invokes onSelectionChange with invalid arguments:

  • Line 68: new Set(['project-1']) — invalid type
  • Line 75: 'all' — invalid type
  • Line 82: null — invalid type

Only line 89 ('project-1' as a string) follows the correct API.

🛠️ Proposed fix
-    onSelectionChange?: (key: React.Key | Set<React.Key> | 'all') => void
+    onSelectionChange?: (key: React.Key) => void

Update mock calls at lines 68, 75, and 82 to pass valid React.Key values instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx` around lines 40 - 85,
The mock's onSelectionChange type and invocations are wrong for Autocomplete:
change the prop signature to onSelectionChange?: (key: React.Key) => void and
update the three buttons that call onSelectionChange to pass single React.Key
values (strings or numbers) instead of a Set, 'all', or null — e.g. use
'project-1', 'project-2', and 'project-none' (or similar single-key strings) for
the handlers on the buttons with data-testids autocomplete-select-item,
autocomplete-select-all, and autocomplete-clear so all calls and the signature
match the Autocomplete API.

40-85: ⚠️ Potential issue | 🟠 Major

Type mismatch: null is not assignable to the declared onSelectionChange signature

The mock's onSelectionChange prop is typed as:

onSelectionChange?: (key: React.Key | Set<React.Key> | 'all') => void

but the clear button at line 82 invokes it with null:

onClick={() => onSelectionChange?.(null)}

null is not a member of string | number | Set<React.Key> | 'all'. This is a TypeScript compile error under strictNullChecks: true. The mock type must include null to permit selection clearing.

🛠️ Proposed fix
-    onSelectionChange?: (key: React.Key | Set<React.Key> | 'all') => void
+    onSelectionChange?: (key: React.Key | Set<React.Key> | 'all' | null) => void
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx` around lines 40 - 85,
The mock prop onSelectionChange's type must allow null because the Clear
Selection button (data-testid="autocomplete-clear") calls
onSelectionChange(null); update the prop signature in the mock component from
onSelectionChange?: (key: React.Key | Set<React.Key> | 'all') => void to include
null — e.g. onSelectionChange?: (key: React.Key | Set<React.Key> | 'all' | null)
=> void — and adjust any related typings/usages of onSelectionChange in the test
mock to match.
🧹 Nitpick comments (12)
frontend/src/app/members/[memberKey]/page.tsx (1)

49-57: dates.at(-1) widens endDate to string | undefined unnecessarily

dates.at(-1) returns T | undefined (ES2022 spec), so endDate is now typed string | undefined even though the dates.length === 0 early-return on lines 51-53 guarantees the array is non-empty by the time line 56 is reached. In practice this can never be undefined, but the widened type forces the extra dateRange.endDate && truthiness guard added at line 166.

Using dates[dates.length - 1] preserves the string type without any assertion:

♻️ Proposed fix
-      endDate: dates.at(-1),
+      endDate: dates[dates.length - 1],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/members/`[memberKey]/page.tsx around lines 49 - 57, The
dateRange useMemo widens endDate to string | undefined due to dates.at(-1);
update the endDate extraction in the useMemo (where dateRange is computed from
contributionData inside the useMemo callback) to use dates[dates.length - 1]
instead of dates.at(-1) so the non-empty-array check (dates.length === 0)
guarantees a string endDate and removes the need for downstream truthiness
guards.
frontend/__tests__/unit/pages/SnapshotDetails.test.tsx (1)

380-403: Test names overstate what's being verified — React key props are not observable from the DOM.

Both tests claim to validate fallback key prop behavior ("uses fallback key", "uses unknown fallback"), but React keys are purely an internal rendering hint and are never exposed in the DOM. @testing-library/react has no mechanism to assert which key was used. What both tests actually verify is that the component renders without crashing when id and/or repositoryName are undefined, and the assertion in both cases is identical:

expect(screen.getByText('v0.9.2')).toBeInTheDocument()

Because the observable outcome is the same regardless of which fallback key is selected, the second test adds only marginal coverage over the first (it confirms no crash occurs with both fields undefined). Renaming to reflect the actual intent would improve readability:

🔤 Suggested renames
-  test('renders release without id (uses fallback key)', async () => {
+  test('renders release without id without crashing', async () => {
-  test('renders release without id and repositoryName (uses unknown fallback)', async () => {
+  test('renders release without id and repositoryName without crashing', async () => {

Also applies to: 405-429

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/SnapshotDetails.test.tsx` around lines 380 -
403, The two tests ('renders release without id (uses fallback key)' and the
similar one at lines 405-429) incorrectly claim to assert React key fallback
behavior, which isn't observable; update the test names to reflect the actual
observable intent (e.g., "renders release without id" and "renders release
without id and repositoryName") and/or strengthen them to assert real
DOM-visible outcomes instead of key usage by keeping the render of
<SnapshotDetailsPage /> and the existing assertion
expect(screen.getByText('v0.9.2')).toBeInTheDocument(), or add additional DOM
checks (e.g., repository text or other visible fields) if you need to
distinguish the two cases; reference the test functions in
SnapshotDetails.test.tsx that mock useQuery and render SnapshotDetailsPage to
locate and rename/adjust the tests.
frontend/__tests__/unit/pages/EditProgram.test.tsx (1)

482-482: Type cast inconsistency: as jest.Mock vs. as unknown as jest.Mock.

Every other useQuery cast in this file uses the double-cast form (as unknown as jest.Mock). The direct as jest.Mock cast here may trigger a TypeScript error in strict mode if the compiler cannot find a structural overlap between useQuery's overloaded type and jest.Mock. Align with the rest of the file:

♻️ Proposed fix
-    ;(useQuery as jest.Mock).mockImplementation(mockQueryFn)
+    ;(useQuery as unknown as jest.Mock).mockImplementation(mockQueryFn)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/EditProgram.test.tsx` at line 482, The test
uses a direct cast "(useQuery as jest.Mock).mockImplementation(mockQueryFn)"
which is inconsistent with the rest of the file and can fail in strict
TypeScript; update the cast to match the other usages by changing the cast to
"useQuery as unknown as jest.Mock" before calling mockImplementation so
useQuery.mockImplementation(mockQueryFn) uses the double-cast form (refer to
useQuery, mockImplementation, and mockQueryFn to locate the line).
frontend/__tests__/unit/pages/EditModule.test.tsx (1)

136-186: Hidden test-order dependency: useMutation is not mocked in two tests.

jest.clearAllMocks() (used in afterEach) clears call history but not mockReturnValue/mockImplementation. The tests at lines 136 and 176 carry no useMutation setup and silently depend on the stale return value left by the first test. If either test is run in isolation (e.g., via .only) or Test 1 is removed/reordered, useMutation() returns undefined and the component crashes with a destructuring TypeError.

Add a default useMutation setup to beforeEach so every test is self-contained:

♻️ Proposed fix
  beforeEach(() => {
    jest.useFakeTimers()
    ;(useRouter as jest.Mock).mockReturnValue({ push: mockPush, replace: mockReplace })
    ;(useParams as jest.Mock).mockReturnValue({
      programKey: 'test-program',
      moduleKey: 'test-module',
    })
    ;(useApolloClient as jest.Mock).mockReturnValue({
      query: jest.fn().mockResolvedValue({
        data: {
          searchProjects: [{ id: '123', name: 'Awesome Project' }],
        },
      }),
    })
+   ;(useMutation as unknown as jest.Mock).mockReturnValue([jest.fn(), { loading: false }])
  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/EditModule.test.tsx` around lines 136 - 186,
Tests have a hidden order dependency because useMutation is not mocked in every
test, causing it to be undefined when tests run in isolation; add a default mock
for useMutation in the test suite setup (e.g., in beforeEach) that returns the
same shape the component expects (a tuple of [mutateFn, resultObj] such as
[jest.fn(), { loading: false, data: null, error: null }]) so destructuring in
the component never throws, and allow individual tests to override that
mockReturnValue where needed; keep afterEach jest.clearAllMocks() as-is.
frontend/__tests__/unit/components/ChapterMap.test.tsx (1)

222-244: jest.clearAllMocks() mid-test and test title don't fully match the intent.

Two things worth noting:

  1. jest.clearAllMocks() at line 237 resets all mock call counts (not just zoom-control ones), making the test state harder to reason about. Splitting into two it blocks — one for first activation and one for re-activation — would keep each assertion self-contained and avoid the silent dependency on what state was established in the first half.

  2. The test title says "removes existing zoom control before adding a new one on re-activation", but in this flow the deactivation branch already sets zoomControlRef.current = null before the second click. So line 29 (zoomControlRef.current?.remove()) is a no-op at the time of re-activation — the specific guard being tested by that production change is never exercised with a non-null ref. The test does verify the correct lifecycle end-to-end, but the title implies coverage that isn't quite there.

♻️ Optional: split into two focused tests
-    it('removes existing zoom control before adding a new one on re-activation', async () => {
-      const { getByText, container } = render(<ChapterMap {...defaultProps} />)
-
-      fireEvent.click(getByText('Unlock map').closest('button'))
-      await waitFor(() => {
-        expect(L.control.zoom).toHaveBeenCalledTimes(1)
-        expect(mockZoomControl.addTo).toHaveBeenCalledTimes(1)
-      })
-
-      fireEvent.mouseLeave(container.firstChild as HTMLElement)
-      await waitFor(() => {
-        expect(mockZoomControl.remove).toHaveBeenCalled()
-        expect(getByText('Unlock map')).toBeInTheDocument()
-      })
-
-      jest.clearAllMocks()
-
-      fireEvent.click(getByText('Unlock map').closest('button'))
-      await waitFor(() => {
-        expect(L.control.zoom).toHaveBeenCalledTimes(1)
-        expect(mockZoomControl.addTo).toHaveBeenCalledTimes(1)
-      })
-    })
+    it('creates zoom control on first activation', async () => {
+      const { getByText } = render(<ChapterMap {...defaultProps} />)
+
+      fireEvent.click(getByText('Unlock map').closest('button'))
+      await waitFor(() => {
+        expect(L.control.zoom).toHaveBeenCalledTimes(1)
+        expect(mockZoomControl.addTo).toHaveBeenCalledTimes(1)
+      })
+    })
+
+    it('recreates zoom control on re-activation after lock', async () => {
+      const { getByText, container } = render(<ChapterMap {...defaultProps} />)
+
+      fireEvent.click(getByText('Unlock map').closest('button'))
+      await waitFor(() => expect(L.control.zoom).toHaveBeenCalledTimes(1))
+
+      fireEvent.mouseLeave(container.firstChild as HTMLElement)
+      await waitFor(() => {
+        expect(mockZoomControl.remove).toHaveBeenCalled()
+        expect(getByText('Unlock map')).toBeInTheDocument()
+      })
+
+      // counts from first cycle are already validated; beforeEach will reset between tests
+      fireEvent.click(getByText('Unlock map').closest('button'))
+      await waitFor(() => {
+        expect(L.control.zoom).toHaveBeenCalledTimes(2) // cumulative: 1 first + 1 second
+        expect(mockZoomControl.addTo).toHaveBeenCalledTimes(2)
+      })
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ChapterMap.test.tsx` around lines 222 -
244, The test mixes two scenarios and uses jest.clearAllMocks() mid-test which
hides mock state and doesn't exercise the non-null guard on
zoomControlRef.current; split into two focused tests (one "activates and
deactivates zoom control" and a second "re-activates and removes existing
control before adding new one"), or alternatively remove jest.clearAllMocks()
and explicitly set zoomControlRef.current to a mock object before the second
click so zoomControlRef.current?.remove() is exercised; refer to L.control.zoom,
mockZoomControl, zoomControlRef.current, and the remove() call when making the
change.
frontend/__tests__/unit/components/ModuleForm.test.tsx (2)

399-421: jest.advanceTimersByTime is a no-op — jest.useFakeTimers() is never called in the ModuleForm block

The ModuleForm describe's beforeEach (lines 279–284) does not install fake timers, so jest.advanceTimersByTime(350) inside act at line 405 has no effect. The debounce runs on the real clock and the test passes only because waitFor's 1 000 ms default timeout exceeds the debounce delay — making these two tests slow and prone to flake under CI load.

The sibling ProjectSelector block already shows the correct pattern; apply it here too:

🛠️ Proposed fix
   beforeEach(() => {
     jest.clearAllMocks()
+    jest.useFakeTimers()
     mockQuery.mockResolvedValue({
       data: { searchProjects: [{ id: 'project-1', name: 'Test Project' }] },
     })
   })
+
+  afterEach(() => {
+    jest.useRealTimers()
+  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx` around lines 399 -
421, The ModuleForm test uses jest.advanceTimersByTime(350) but never installs
fake timers; in the ModuleForm describe block add jest.useFakeTimers() in its
beforeEach (the same place renderModuleForm is prepared) and add
jest.useRealTimers() in afterEach to restore timers; ensure the advanceTimers
call is wrapped in act (as it already is) and that any pending timers are
flushed (e.g., runOnlyPendingTimers or advanceTimersByTime) so the debounce in
ProjectSelector triggers deterministically when calling renderModuleForm and
firing change/click events.

399-421: jest.advanceTimersByTime is a no-op here — fake timers are never installed in the ModuleForm block

The ModuleForm describe's beforeEach (lines 279–284) only calls jest.clearAllMocks() and sets up mockQuery; it never calls jest.useFakeTimers(). Inside act, jest.advanceTimersByTime(350) therefore has no effect, and the debounce inside ProjectSelector fires on real time. The test passes only because waitFor's default 1 000 ms timeout outlasts the ~300 ms debounce — this is fragile and will be unreliable under CI load.

Add jest.useFakeTimers() / jest.useRealTimers() to the ModuleForm describe's beforeEach / afterEach:

🛠️ Proposed fix
   beforeEach(() => {
     jest.clearAllMocks()
+    jest.useFakeTimers()
     mockQuery.mockResolvedValue({
       data: { searchProjects: [{ id: 'project-1', name: 'Test Project' }] },
     })
   })
+
+  afterEach(() => {
+    jest.useRealTimers()
+  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx` around lines 399 -
421, The test uses jest.advanceTimersByTime(350) but fake timers are never
enabled in the ModuleForm describe; update the ModuleForm describe's setup to
call jest.useFakeTimers() in its beforeEach and restore timers with
jest.useRealTimers() in its afterEach so that the debounce in ProjectSelector
responds to the timer advance inside the act block (the area around the existing
beforeEach/afterEach that sets up mockQuery and jest.clearAllMocks()); this
ensures the jest.advanceTimersByTime(350) in the 'updates project field when
ProjectSelector changes' test actually triggers the debounce rather than relying
on real time.
frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx (1)

655-678: Consider asserting the button's disabled state before clicking to make preconditions explicit.

The test relies on disabled={!issueId || assigning} preventing the click, but doesn't assert that precondition. Adding a toBeDisabled() check before fireEvent.click makes the intent self-documenting and would catch regressions if the disabled condition were inadvertently removed.

✅ Suggested assertion
  const assignButton = within(userGrid as HTMLElement).getByRole('button', { name: /Assign/i })
+ expect(assignButton).toBeDisabled()
  fireEvent.click(assignButton)
  expect(assignIssue).not.toHaveBeenCalled()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx` around lines
655 - 678, Add an explicit assertion that the Assign button is disabled before
clicking to make the precondition clear and prevent false positives: in the test
for ModuleIssueDetailsPage (where mockUseParams sets issueId to '' and
mockUseIssueMutations returns assigning: false and assignIssue is a jest.fn),
locate the retrieved Assign button (within the userGrid) and assert
expect(assignButton).toBeDisabled() before fireEvent.click(assignButton); keep
the existing expect(assignIssue).not.toHaveBeenCalled() afterwards to verify no
mutation occurred.
frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx (1)

228-231: Removed onClick guards rely entirely on disabled — consider retaining a guard on mutation-invoking handlers.

Removing if (!canEditDeadline) return from the deadline onClick is safe here because isEditingDeadline && canEditDeadline (line 182) still gates the date input from appearing, and React does not dispatch onClick for disabled buttons. However, the same approach on the unassign (lines 300–307) and assign (lines 367–374) handlers removes the last programmatic barrier before an actual GraphQL mutation fires. If the button's disabled condition were ever mis-set or bypassed (e.g., programmatically), Number(issueId) with an empty issueId evaluates to 0 — a silent, valid-looking integer that would be forwarded to the mutation with no error at the call site.

For the deadline button, the removal is fine. For the two mutation handlers, a minimal guard preserves defense-in-depth at negligible cost:

🛡️ Suggested guard on mutation handlers
// Unassign button onClick
 onClick={async () => {
+  if (!issueId || unassigning) return
   await unassignIssue({ variables: { programKey, moduleKey, issueNumber: Number(issueId), userLogin: a.login } })
 }}

// Assign button onClick
 onClick={async () => {
+  if (!issueId || assigning) return
   await assignIssue({ variables: { programKey, moduleKey, issueNumber: Number(issueId), userLogin: u.login } })
 }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/my/mentorship/programs/`[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx
around lines 228 - 231, Add a defensive early-return to the assign and unassign
click handlers: before calling the GraphQL mutation (the code that constructs
Number(issueId) and invokes the assign/unassign mutation), check the same
permission/guard used to disable the buttons (e.g., if (!canEditAssignees)
return) and also validate issueId (e.g., if (!issueId) return) so you never call
Number(issueId) or fire the mutation when the action is not allowed or issueId
is empty; keep the deadline click behavior unchanged.
frontend/__tests__/unit/components/CardDetailsPage.test.tsx (1)

1617-1623: Split into two it blocks for clarity.

Two independent scenarios (undefined topics vs undefined languages) are combined in a single test with a manual cleanup() between renders. When this test fails it's harder to identify which scenario is broken. Each scenario is self-contained and deserves its own test.

♻️ Suggested split
-    it('handles undefined topics/languages in grid layout', () => {
-      render(<CardDetailsPage {...defaultProps} languages={['JavaScript']} topics={undefined} />)
-      expect(screen.getByTestId('toggleable-list')).toHaveTextContent('Languages: JavaScript')
-      cleanup()
-      render(<CardDetailsPage {...defaultProps} languages={undefined} topics={['web']} />)
-      expect(screen.getByTestId('toggleable-list')).toHaveTextContent('Topics: web')
-    })
+    it('renders only languages when topics is undefined', () => {
+      render(<CardDetailsPage {...defaultProps} languages={['JavaScript']} topics={undefined} />)
+      expect(screen.getByTestId('toggleable-list')).toHaveTextContent('Languages: JavaScript')
+    })
+
+    it('renders only topics when languages is undefined', () => {
+      render(<CardDetailsPage {...defaultProps} languages={undefined} topics={['web']} />)
+      expect(screen.getByTestId('toggleable-list')).toHaveTextContent('Topics: web')
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/CardDetailsPage.test.tsx` around lines
1617 - 1623, Split the combined test "handles undefined topics/languages in grid
layout" into two independent it blocks: one that renders CardDetailsPage with
languages=['JavaScript'] and topics=undefined and asserts the toggleable-list
contains "Languages: JavaScript", and a second that renders CardDetailsPage with
languages=undefined and topics=['web'] and asserts the toggleable-list contains
"Topics: web"; remove the manual cleanup() between renders so each test is
self-contained and has a clear, descriptive name (e.g., "renders languages when
topics undefined" and "renders topics when languages undefined") to make
failures easier to diagnose.
frontend/__tests__/unit/pages/UserDetails.test.tsx (2)

1031-1051: Test name "validates defensive check for endDate" is unclear.

The name exposes an implementation detail rather than describing the observable behavior. A reader cannot tell what's being tested without reading the body.

💡 Suggested rename
-    test('validates defensive check for endDate', async () => {
+    test('renders heatmap when contributionData contains a single entry', async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/UserDetails.test.tsx` around lines 1031 - 1051,
The test name "validates defensive check for endDate" leaks an implementation
detail and is unclear; rename the test in UserDetails.test.tsx to describe
observable behavior (e.g., "renders contribution heatmap when contribution data
has a single date" or "shows heatmap even when contributionData contains only
one date"), updating the test declaration that contains the mock
singleDateContribution and the render of UserDetailsPage so it reads the new
descriptive name while keeping the existing mock and assertions (references: the
test block currently named 'validates defensive check for endDate', the
singleDateContribution mock, and the render(<UserDetailsPage />) /
expect(screen.getByTestId('contribution-heatmap')).toBeInTheDocument()).

1053-1075: Near-duplicate of existing tests at lines 895–917 and 942–963.

The new test uses login: 'fallback-login' (instead of the default 'testuser') to show the fallback is dynamic, which is marginally useful — but the code path (name: null → use login) and both assertion types (avatar alt text, displayed text) are already exercised by the pair of existing tests. Consider removing this test or merging its getByText assertion into one of the existing tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/UserDetails.test.tsx` around lines 1053 - 1075,
This test ("uses user login as fallback for avatar alt text and title when name
is missing") is a near-duplicate of existing tests that already cover the code
path when user.name is null and assert avatar alt text and displayed login;
remove this redundant test or merge its unique assertion (the
getByText('fallback-login') check) into one of the existing tests that mock
userWithoutName (reuse mockUserDetailsData and the useQuery mock) so the
fallback-login dynamic case is still covered without adding a duplicate test
case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/__tests__/unit/components/CardDetailsPage.test.tsx`:
- Around line 1625-1634: The test fails TypeScript because you assign value:
null while DetailsCardProps.details expects string | JSX.Element; update the
test to avoid the type error by either removing the explicit DetailsCardProps
annotation so TypeScript infers the literal (like other tests) or cast the null
value (e.g., value: null as unknown as any) when constructing
propsWithNullLeader used by CardDetailsPage; keep references to defaultProps and
CardDetailsPage intact so the test still renders and asserts "Leaders:" and
"Unknown".

In `@frontend/__tests__/unit/contexts/BreadcrumbContext.test.tsx`:
- Around line 54-58: The test calling registerBreadcrumbClassName('test') should
push its unregister callback into the shared cleanupFns so teardown runs even if
the assertion fails; update the test to capture the unregister from
registerBreadcrumbClassName('test') and call cleanupFns.push(unregister) (or
equivalent) instead of calling unregister() directly, ensuring the module-level
classNameStack is cleared in afterEach; locate registerBreadcrumbClassName and
the test in BreadcrumbContext.test.tsx and use the existing cleanupFns helper to
register the unregister function for deferred cleanup.

In `@frontend/__tests__/unit/pages/EditProgram.test.tsx`:
- Around line 458-491: The test currently relies on mockReturnValueOnce calls on
mockQueryFn which assumes an exact number of renders; instead implement
mockQueryFn as a closure with an internal call counter and useQuery mocked via
mockImplementation to call that closure so responses are deterministic: have the
closure return the validData response for the first N calls (e.g., 3) and then
return the nullAdminsData response thereafter. Update references to mockQueryFn
and useQuery in the test to use this closure-based mock so rerender()
transitions predictably regardless of React render counts.

In `@frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx`:
- Around line 545-561: The test MenteeProfilePage.test.tsx currently spies on
React.useState and calls jest.restoreAllMocks() inline, which can leak the spy
if assertions throw; change the test to guarantee cleanup by either moving spy
restoration into an afterEach that calls jest.restoreAllMocks() (or at minimum
wrap the render/assert block in a try/finally that calls
jest.restoreAllMocks()), and replace the fragile React.useState interception
with a safer approach: drive the invalid filter scenario through the existing
Select testing pattern (use the select-trigger-empty helper or add a hidden
trigger that calls onSelectionChange(new Set(['invalid_filter']))), so you no
longer need to spy on React.useState and avoid hook-count/side-effect issues.

In `@frontend/__tests__/unit/pages/MyMentorship.test.tsx`:
- Around line 449-455: The test is enabling jest.useFakeTimers() mid-test which
leaves a real timer queued; move jest.useFakeTimers() to run before the
component is rendered (matching the "updates debounced search query" test) and
remove the duplicate fireEvent.change so only one change is dispatched under
fake timers, then advance the fake timer with jest.advanceTimersByTime(500)
inside React.act; update any setup/teardown to restore timers if present.

In `@frontend/src/app/my/mentorship/programs/`[programKey]/edit/page.tsx:
- Line 121: Restore the runtime fallback for the program status before sending
the GraphQL mutation: ensure the value passed as status uses formData.status ??
ProgramStatusEnum.Draft rather than only casting with as ProgramStatusEnum so
the mutation (UpdateProgramInput) never receives undefined; update the
assignment where status is built (the line currently using status:
formData.status as ProgramStatusEnum) to use the nullish fallback to
ProgramStatusEnum.Draft to provide a real runtime enum value.

In
`@frontend/src/app/my/mentorship/programs/`[programKey]/modules/[moduleKey]/issues/page.tsx:
- Around line 117-120: The onSelectionChange handler currently destructures an
empty Set into key and calls handleLabelChange(key as string), which lets
runtime undefined propagate into selectedLabel and URL; update the
onSelectionChange in the Select to check that the extracted key is
defined/truthy before calling handleLabelChange (e.g., const [key] =
Array.from(keys as Set<string>); if (!key) return; handleLabelChange(key)); this
restores the previous undefined guard so selectedLabel (used to build new
Set([selectedLabel])) and params.set('label', ...) never receive the literal
"undefined" string and GraphQL queries keep correct null semantics.

---

Outside diff comments:
In `@frontend/__tests__/unit/components/ModuleForm.test.tsx`:
- Around line 657-687: The test currently only asserts the input change
side-effect and never verifies the Set-based selection path; update the test
around the click on the element with test id "autocomplete-select-item" (which
triggers onSelectionChange(new Set(['project-1'])) and is handled by
handleSelectionChange) to also assert that mockOnProjectChange was called with
the selected project ID and name
(expect(mockOnProjectChange).toHaveBeenCalledWith('project-1', 'Test Project
1')), using waitFor after the click to account for async handling.
- Around line 657-687: The test "handles selection via Set" only asserts the
typing side-effect and misses verifying the Set-selection path: after clicking
the element with test id "autocomplete-select-item" (which triggers
onSelectionChange(new Set(['project-1']))) add an assertion that
mockOnProjectChange was called for the selection, e.g.
expect(mockOnProjectChange).toHaveBeenCalledWith('project-1', 'Test Project 1');
place this check after the act/click and any waitFor so it validates the
handleSelectionChange -> onProjectChange flow invoked by the selection; keep the
existing typing assertions (renderProjectSelector, mockQuery, input change)
intact.
- Around line 40-85: The mock's onSelectionChange type and invocations are wrong
for Autocomplete: change the prop signature to onSelectionChange?: (key:
React.Key) => void and update the three buttons that call onSelectionChange to
pass single React.Key values (strings or numbers) instead of a Set, 'all', or
null — e.g. use 'project-1', 'project-2', and 'project-none' (or similar
single-key strings) for the handlers on the buttons with data-testids
autocomplete-select-item, autocomplete-select-all, and autocomplete-clear so all
calls and the signature match the Autocomplete API.
- Around line 40-85: The mock prop onSelectionChange's type must allow null
because the Clear Selection button (data-testid="autocomplete-clear") calls
onSelectionChange(null); update the prop signature in the mock component from
onSelectionChange?: (key: React.Key | Set<React.Key> | 'all') => void to include
null — e.g. onSelectionChange?: (key: React.Key | Set<React.Key> | 'all' | null)
=> void — and adjust any related typings/usages of onSelectionChange in the test
mock to match.

In `@frontend/__tests__/unit/pages/EditModule.test.tsx`:
- Line 188: The test case name is stale: the it(...) titled 'shows loading
spinner when query returns an error' no longer matches the assertion (which
checks for 'Access Denied'); update the test description string to reflect the
actual expectation (e.g., 'shows Access Denied when query returns an error' or
similar) so the test name matches the assertion in EditModule.test.tsx (locate
the it(...) block around the assertion that looks for 'Access Denied').

---

Nitpick comments:
In `@frontend/__tests__/unit/components/CardDetailsPage.test.tsx`:
- Around line 1617-1623: Split the combined test "handles undefined
topics/languages in grid layout" into two independent it blocks: one that
renders CardDetailsPage with languages=['JavaScript'] and topics=undefined and
asserts the toggleable-list contains "Languages: JavaScript", and a second that
renders CardDetailsPage with languages=undefined and topics=['web'] and asserts
the toggleable-list contains "Topics: web"; remove the manual cleanup() between
renders so each test is self-contained and has a clear, descriptive name (e.g.,
"renders languages when topics undefined" and "renders topics when languages
undefined") to make failures easier to diagnose.

In `@frontend/__tests__/unit/components/ChapterMap.test.tsx`:
- Around line 222-244: The test mixes two scenarios and uses
jest.clearAllMocks() mid-test which hides mock state and doesn't exercise the
non-null guard on zoomControlRef.current; split into two focused tests (one
"activates and deactivates zoom control" and a second "re-activates and removes
existing control before adding new one"), or alternatively remove
jest.clearAllMocks() and explicitly set zoomControlRef.current to a mock object
before the second click so zoomControlRef.current?.remove() is exercised; refer
to L.control.zoom, mockZoomControl, zoomControlRef.current, and the remove()
call when making the change.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx`:
- Around line 399-421: The ModuleForm test uses jest.advanceTimersByTime(350)
but never installs fake timers; in the ModuleForm describe block add
jest.useFakeTimers() in its beforeEach (the same place renderModuleForm is
prepared) and add jest.useRealTimers() in afterEach to restore timers; ensure
the advanceTimers call is wrapped in act (as it already is) and that any pending
timers are flushed (e.g., runOnlyPendingTimers or advanceTimersByTime) so the
debounce in ProjectSelector triggers deterministically when calling
renderModuleForm and firing change/click events.
- Around line 399-421: The test uses jest.advanceTimersByTime(350) but fake
timers are never enabled in the ModuleForm describe; update the ModuleForm
describe's setup to call jest.useFakeTimers() in its beforeEach and restore
timers with jest.useRealTimers() in its afterEach so that the debounce in
ProjectSelector responds to the timer advance inside the act block (the area
around the existing beforeEach/afterEach that sets up mockQuery and
jest.clearAllMocks()); this ensures the jest.advanceTimersByTime(350) in the
'updates project field when ProjectSelector changes' test actually triggers the
debounce rather than relying on real time.

In `@frontend/__tests__/unit/pages/EditModule.test.tsx`:
- Around line 136-186: Tests have a hidden order dependency because useMutation
is not mocked in every test, causing it to be undefined when tests run in
isolation; add a default mock for useMutation in the test suite setup (e.g., in
beforeEach) that returns the same shape the component expects (a tuple of
[mutateFn, resultObj] such as [jest.fn(), { loading: false, data: null, error:
null }]) so destructuring in the component never throws, and allow individual
tests to override that mockReturnValue where needed; keep afterEach
jest.clearAllMocks() as-is.

In `@frontend/__tests__/unit/pages/EditProgram.test.tsx`:
- Line 482: The test uses a direct cast "(useQuery as
jest.Mock).mockImplementation(mockQueryFn)" which is inconsistent with the rest
of the file and can fail in strict TypeScript; update the cast to match the
other usages by changing the cast to "useQuery as unknown as jest.Mock" before
calling mockImplementation so useQuery.mockImplementation(mockQueryFn) uses the
double-cast form (refer to useQuery, mockImplementation, and mockQueryFn to
locate the line).

In `@frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx`:
- Around line 655-678: Add an explicit assertion that the Assign button is
disabled before clicking to make the precondition clear and prevent false
positives: in the test for ModuleIssueDetailsPage (where mockUseParams sets
issueId to '' and mockUseIssueMutations returns assigning: false and assignIssue
is a jest.fn), locate the retrieved Assign button (within the userGrid) and
assert expect(assignButton).toBeDisabled() before fireEvent.click(assignButton);
keep the existing expect(assignIssue).not.toHaveBeenCalled() afterwards to
verify no mutation occurred.

In `@frontend/__tests__/unit/pages/SnapshotDetails.test.tsx`:
- Around line 380-403: The two tests ('renders release without id (uses fallback
key)' and the similar one at lines 405-429) incorrectly claim to assert React
key fallback behavior, which isn't observable; update the test names to reflect
the actual observable intent (e.g., "renders release without id" and "renders
release without id and repositoryName") and/or strengthen them to assert real
DOM-visible outcomes instead of key usage by keeping the render of
<SnapshotDetailsPage /> and the existing assertion
expect(screen.getByText('v0.9.2')).toBeInTheDocument(), or add additional DOM
checks (e.g., repository text or other visible fields) if you need to
distinguish the two cases; reference the test functions in
SnapshotDetails.test.tsx that mock useQuery and render SnapshotDetailsPage to
locate and rename/adjust the tests.

In `@frontend/__tests__/unit/pages/UserDetails.test.tsx`:
- Around line 1031-1051: The test name "validates defensive check for endDate"
leaks an implementation detail and is unclear; rename the test in
UserDetails.test.tsx to describe observable behavior (e.g., "renders
contribution heatmap when contribution data has a single date" or "shows heatmap
even when contributionData contains only one date"), updating the test
declaration that contains the mock singleDateContribution and the render of
UserDetailsPage so it reads the new descriptive name while keeping the existing
mock and assertions (references: the test block currently named 'validates
defensive check for endDate', the singleDateContribution mock, and the
render(<UserDetailsPage />) /
expect(screen.getByTestId('contribution-heatmap')).toBeInTheDocument()).
- Around line 1053-1075: This test ("uses user login as fallback for avatar alt
text and title when name is missing") is a near-duplicate of existing tests that
already cover the code path when user.name is null and assert avatar alt text
and displayed login; remove this redundant test or merge its unique assertion
(the getByText('fallback-login') check) into one of the existing tests that mock
userWithoutName (reuse mockUserDetailsData and the useQuery mock) so the
fallback-login dynamic case is still covered without adding a duplicate test
case.

In `@frontend/src/app/members/`[memberKey]/page.tsx:
- Around line 49-57: The dateRange useMemo widens endDate to string | undefined
due to dates.at(-1); update the endDate extraction in the useMemo (where
dateRange is computed from contributionData inside the useMemo callback) to use
dates[dates.length - 1] instead of dates.at(-1) so the non-empty-array check
(dates.length === 0) guarantees a string endDate and removes the need for
downstream truthiness guards.

In
`@frontend/src/app/my/mentorship/programs/`[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx:
- Around line 228-231: Add a defensive early-return to the assign and unassign
click handlers: before calling the GraphQL mutation (the code that constructs
Number(issueId) and invokes the assign/unassign mutation), check the same
permission/guard used to disable the buttons (e.g., if (!canEditAssignees)
return) and also validate issueId (e.g., if (!issueId) return) so you never call
Number(issueId) or fire the mutation when the action is not allowed or issueId
is empty; keep the deadline click behavior unchanged.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 20 files

Confidence score: 3/5

  • There’s some user-facing risk: removing the guard in frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx can let handleLabelChange receive undefined when the selection is cleared, which could break label changes.
  • Test hygiene has a mild risk: frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx may leak a React.useState spy on failure, potentially affecting subsequent tests.
  • Pay close attention to frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx - empty selection handling could pass undefined to handleLabelChange.
Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/__tests__/unit/pages/SnapshotDetails.test.tsx">

<violation number="1" location="frontend/__tests__/unit/pages/SnapshotDetails.test.tsx:405">
P2: Test name claims to validate 'unknown fallback' but only asserts release version is rendered. The test setup passes both id and repositoryName as undefined, but the assertion only checks that 'v0.9.2' appears, which doesn't verify whether the fallback behavior for missing fields actually works.</violation>
</file>

<file name="frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx">

<violation number="1" location="frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx:119">
P1: Removing the guard allows `handleLabelChange` to receive undefined when selection is empty (e.g., user clicks the same option twice). HeroUI Select's `disallowEmptySelection` defaults to `false`, allowing empty selections. This results in `label=undefined` in the URL and incorrect backend filtering.</violation>
</file>

<file name="frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx">

<violation number="1" location="frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx:560">
P2: Mock cleanup is not guaranteed on test failure - React.useState spy restoration may leak to subsequent tests</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
frontend/__tests__/unit/contexts/BreadcrumbContext.test.tsx (1)

54-58: The cleanup fix is correct; consider strengthening the assertion.

cleanupFns.push(unregister) before the expect correctly addresses the prior teardown concern.

The lone assertion expect(unregister).toBeDefined() can never fail — registerBreadcrumbClassName is typed () => void, so it unconditionally returns a function. If the context ever exposes a class-name getter (e.g. getBreadcrumbClassNames()), consider replacing or supplementing this with a behavioral assertion such as verifying the registered class name is present and absent after afterEach cleanup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/contexts/BreadcrumbContext.test.tsx` around lines 54
- 58, The test currently only asserts the unregister function exists; strengthen
it by asserting observable behavior: after calling
registerBreadcrumbClassName('test') (and pushing unregister into cleanupFns),
verify the registered class name is actually present via the context's accessor
(e.g., getBreadcrumbClassNames() or similar) or by checking the relevant DOM
element's class list, then call the returned unregister and assert the class
name is removed; use the existing registerBreadcrumbClassName and
cleanupFns/unregister helpers to locate where to add these presence/absence
assertions.
frontend/__tests__/unit/pages/SnapshotDetails.test.tsx (1)

380-403: Consider strengthening the assertion to differentiate this test from the "without repositoryName" variant.

releaseWithoutId still carries repositoryName: 'test-project-1' from the spread, so the test could additionally assert that 'test-project-1' is visible. Currently the single getByText('v0.9.2') assertion doesn't confirm that the repository name still renders correctly when only id is absent — which is the meaningful distinction between this test and the one at line 405.

🔍 Suggested additional assertion
  await waitFor(() => {
    expect(screen.getByText('v0.9.2')).toBeInTheDocument()
+   expect(screen.getByText('test-project-1')).toBeInTheDocument()
  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/SnapshotDetails.test.tsx` around lines 380 -
403, The test "renders release without id (uses fallback key)" creates
releaseWithoutId from mockSnapshotDetailsData.snapshot.newReleases[0] but never
asserts the repository name; update the test (in SnapshotDetails.test.tsx) to
also assert that the repositoryName from releaseWithoutId (e.g.,
'test-project-1') is rendered by the SnapshotDetailsPage component—use the
existing screen/getByText or waitFor to check that the repository name is in the
document so this case is distinguished from the "without repositoryName"
variant.
frontend/src/app/members/[memberKey]/page.tsx (2)

57-57: badges.slice() is redundant

.map() already returns a new array without mutating the source; the extra .slice() just allocates a throw-away copy.

♻️ Proposed fix
-              {badges.slice().map((badge: Badge) => (
+              {badges.map((badge: Badge) => (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/members/`[memberKey]/page.tsx at line 57, The call to
badges.slice() before mapping is redundant and wastes an allocation; in the
component rendering where badges.slice().map((badge: Badge) => ...) is used,
remove the .slice() and use badges.map((badge: Badge) => ...) instead so the
original array isn't copied unnecessarily while preserving behavior; update any
related occurrences of badges.slice().map to badges.map in this file (page.tsx)
or nearby render helpers.

118-126: dates.at(-1) gives an imprecise string | undefined type despite the non-empty guard

The branch guarding dates.length === 0 guarantees the array is non-empty when dates.at(-1) is called, yet TypeScript infers string | undefined for .at(), making endDate's type looser than necessary. Using the index form instead keeps the inferred type string and makes intent explicit.

♻️ Proposed fix
     return {
       startDate: dates[0],
-      endDate: dates.at(-1),
+      endDate: dates[dates.length - 1],
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/members/`[memberKey]/page.tsx around lines 118 - 126, The
computed dateRange inside the useMemo uses dates.at(-1) which TypeScript types
as string | undefined despite the earlier guard; replace the .at(-1) call with
the explicit indexed access dates[dates.length - 1] (or otherwise assert
non-null) so endDate is inferred as string; update the return object in the
dateRange computation (the useMemo that reads contributionData and builds dates)
to use dates[dates.length - 1] instead of dates.at(-1).
frontend/__tests__/unit/pages/EditProgram.test.tsx (1)

460-460: Redundant jest.clearAllMocks() call

beforeEach already calls jest.clearAllMocks() as its last statement (Line 50), so this call adds no value. jest.clearAllMocks() resets all information stored in the mock function's usage data but does not change the mock implementation or return value. Since no mocks have been called between the end of beforeEach and this line, the call history is already empty.

🧹 Proposed cleanup
-    jest.clearAllMocks()
     ;(useSession as jest.Mock).mockReturnValue({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/EditProgram.test.tsx` at line 460, Remove the
redundant jest.clearAllMocks() call at the test location — since the test
suite's beforeEach already invokes jest.clearAllMocks() (see the beforeEach
block), delete the extra jest.clearAllMocks() line to avoid unnecessary
duplication and keep setup/teardown centralized; search for the standalone
jest.clearAllMocks() at the location indicated and remove it, leaving the
beforeEach-managed clearing intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@frontend/__tests__/unit/contexts/BreadcrumbContext.test.tsx`:
- Around line 54-58: The test currently only asserts the unregister function
exists; strengthen it by asserting observable behavior: after calling
registerBreadcrumbClassName('test') (and pushing unregister into cleanupFns),
verify the registered class name is actually present via the context's accessor
(e.g., getBreadcrumbClassNames() or similar) or by checking the relevant DOM
element's class list, then call the returned unregister and assert the class
name is removed; use the existing registerBreadcrumbClassName and
cleanupFns/unregister helpers to locate where to add these presence/absence
assertions.

In `@frontend/__tests__/unit/pages/EditProgram.test.tsx`:
- Line 460: Remove the redundant jest.clearAllMocks() call at the test location
— since the test suite's beforeEach already invokes jest.clearAllMocks() (see
the beforeEach block), delete the extra jest.clearAllMocks() line to avoid
unnecessary duplication and keep setup/teardown centralized; search for the
standalone jest.clearAllMocks() at the location indicated and remove it, leaving
the beforeEach-managed clearing intact.

In `@frontend/__tests__/unit/pages/SnapshotDetails.test.tsx`:
- Around line 380-403: The test "renders release without id (uses fallback key)"
creates releaseWithoutId from mockSnapshotDetailsData.snapshot.newReleases[0]
but never asserts the repository name; update the test (in
SnapshotDetails.test.tsx) to also assert that the repositoryName from
releaseWithoutId (e.g., 'test-project-1') is rendered by the SnapshotDetailsPage
component—use the existing screen/getByText or waitFor to check that the
repository name is in the document so this case is distinguished from the
"without repositoryName" variant.

In `@frontend/src/app/members/`[memberKey]/page.tsx:
- Line 57: The call to badges.slice() before mapping is redundant and wastes an
allocation; in the component rendering where badges.slice().map((badge: Badge)
=> ...) is used, remove the .slice() and use badges.map((badge: Badge) => ...)
instead so the original array isn't copied unnecessarily while preserving
behavior; update any related occurrences of badges.slice().map to badges.map in
this file (page.tsx) or nearby render helpers.
- Around line 118-126: The computed dateRange inside the useMemo uses
dates.at(-1) which TypeScript types as string | undefined despite the earlier
guard; replace the .at(-1) call with the explicit indexed access
dates[dates.length - 1] (or otherwise assert non-null) so endDate is inferred as
string; update the return object in the dateRange computation (the useMemo that
reads contributionData and builds dates) to use dates[dates.length - 1] instead
of dates.at(-1).

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 21, 2026
cubic-dev-ai[bot]
cubic-dev-ai bot previously approved these changes Feb 21, 2026
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 20 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

@kasya
Copy link
Collaborator

kasya commented Feb 21, 2026

@HarshitVerma109 hi! Please resolve conflicts 🙏🏼

kasya
kasya previously approved these changes Feb 21, 2026
Copy link
Collaborator

@kasya kasya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HarshitVerma109 looks good 👍🏼
Make sure to resolve conflicts so that we could merge this.

@HarshitVerma109 HarshitVerma109 force-pushed the feat/improve-frontend-test-coverage branch from 4391ca1 to 06bf360 Compare February 22, 2026 00:35
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/__tests__/unit/components/ModuleForm.test.tsx (1)

45-45: ⚠️ Potential issue | 🟡 Minor

null argument at line 82 does not match the onSelectionChange type declared at line 45

The declared type excludes null (React.Key | Set<React.Key> | 'all'), but the Clear Selection button at line 82 passes null. HeroUI Autocomplete actually emits null when clearing, so the mock's type signature must include it for correctness:

🛠️ Proposed fix
-    onSelectionChange?: (key: React.Key | Set<React.Key> | 'all') => void
+    onSelectionChange?: (key: React.Key | Set<React.Key> | 'all' | null) => void
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx` at line 45, The test
mock's onSelectionChange signature doesn't accept null while the component
(HeroUI Autocomplete) can emit null when clearing; update the mock/type to
include null (i.e., change the onSelectionChange type to React.Key |
Set<React.Key> | 'all' | null) or adjust the mock implementation for
onSelectionChange used in ModuleForm.test.tsx to accept a null argument
(reference the onSelectionChange prop and the Clear Selection button/mock that
passes null).
🧹 Nitpick comments (7)
frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/edit/page.tsx (1)

100-112: Consider replacing non-null assertions with a null guard at the top of handleSubmit.

The assertions are logically safe today because ModuleForm (and therefore its submit handler) is only mounted when formData !== null (lines 145-147). But scattering formData! across a dozen field accesses makes the code brittle: any future refactor that calls handleSubmit directly (e.g., a keyboard shortcut, a test, or an imperative trigger) will throw at runtime without a TypeScript-visible warning.

A single guard at the top of the function is more readable and resilient:

♻️ Proposed refactor — replace assertions with an early return guard
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
+   if (!formData) return

    try {
      const input: UpdateModuleInput = {
-       description: formData!.description,
-       domains: parseCommaSeparated(formData!.domains),
-       endedAt: formData!.endedAt || '',
-       experienceLevel: formData!.experienceLevel as ExperienceLevelEnum,
+       description: formData.description,
+       domains: parseCommaSeparated(formData.domains),
+       endedAt: formData.endedAt || '',
+       experienceLevel: formData.experienceLevel as ExperienceLevelEnum,
        key: moduleKey,
-       labels: parseCommaSeparated(formData!.labels),
-       mentorLogins: parseCommaSeparated(formData!.mentorLogins),
-       name: formData!.name,
+       labels: parseCommaSeparated(formData.labels),
+       mentorLogins: parseCommaSeparated(formData.mentorLogins),
+       name: formData.name,
        programKey: programKey,
-       projectId: formData!.projectId,
-       projectName: formData!.projectName,
-       startedAt: formData!.startedAt || '',
-       tags: parseCommaSeparated(formData!.tags),
+       projectId: formData.projectId,
+       projectName: formData.projectName,
+       startedAt: formData.startedAt || '',
+       tags: parseCommaSeparated(formData.tags),
      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/my/mentorship/programs/`[programKey]/modules/[moduleKey]/edit/page.tsx
around lines 100 - 112, The handleSubmit implementation currently uses repeated
non-null assertions (formData!) when building the payload; add a single null
guard at the top of handleSubmit (e.g., if (!formData) { return; } or throw new
Error) and then remove the individual non-null assertions so the rest of the
function can safely reference formData fields (description, domains, endedAt,
experienceLevel, labels, mentorLogins, name, projectId, projectName, startedAt,
tags, etc.); update any callers/tests if they rely on throwing behavior to
instead handle the early return or thrown error.
frontend/__tests__/unit/pages/EditProgram.test.tsx (1)

436-478: Redundant jest.clearAllMocks() inside the test body

beforeEach already calls jest.clearAllMocks() as its final statement (line 50), so the same call at line 460 is a no-op. jest.clearAllMocks only wipes call history, not mockReturnValue/mockImplementation state, so nothing breaks — it's just dead code.

🧹 Suggested cleanup
-    jest.clearAllMocks()
-    ;(useSession as jest.Mock).mockReturnValue({
+    ;(useSession as jest.Mock).mockReturnValue({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/EditProgram.test.tsx` around lines 436 - 478,
The jest.clearAllMocks() call inside the test "handles null admins when access
is initially allowed" is redundant because beforeEach already calls
jest.clearAllMocks(); remove the in-test call to jest.clearAllMocks() from
EditProgram.test.tsx (inside that specific test) and keep the existing
beforeEach behavior so mocks are cleared consistently; no other changes needed.
frontend/__tests__/unit/pages/MyMentorship.test.tsx (1)

423-463: jest.useFakeTimers() placement is now correct — previous issue resolved.

The timer is activated before render (line 445 → render line 447), matching the "updates debounced search query" pattern and eliminating the earlier real-timer/fake-timer race. The waitFor(() => expect(mockPush).not.toHaveBeenCalled()) assertion is safe under fake timers here because the debounce fires inside React.act(() => jest.advanceTimersByTime(500)) before waitFor is entered, so waitFor's own internal retry timer (which can stall under fake timers) is never needed.

The useSearchParams mock (line 442) and history.replaceState (line 443) are set up outside the try block, while the "updates debounced search query" sibling keeps all setup inside try. Moving them inside would make the two fake-timer tests consistent:

♻️ Optional — align setup placement with the sibling test
-  ;(useSearchParams as jest.Mock).mockReturnValue(new URLSearchParams('q=existing'))
-  globalThis.history.replaceState(null, '', '?q=existing')
-
   jest.useFakeTimers()
   try {
+    ;(useSearchParams as jest.Mock).mockReturnValue(new URLSearchParams('q=existing'))
+    globalThis.history.replaceState(null, '', '?q=existing')
+
     render(<MyMentorshipPage />)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/MyMentorship.test.tsx` around lines 423 - 463,
Move the setup of the useSearchParams mock and globalThis.history.replaceState
into the try block alongside jest.useFakeTimers() and render so the two
fake-timer tests follow the same setup/teardown pattern; specifically, for the
test case that calls jest.useFakeTimers() and renders MyMentorshipPage, relocate
the lines that call (useSearchParams as jest.Mock).mockReturnValue(...) and
globalThis.history.replaceState(...) inside the try before render(), and keep
the existing finally block that restores history and calls jest.useRealTimers()
to ensure consistent isolation for the debounced search behavior.
frontend/__tests__/unit/components/CardDetailsPage.test.tsx (1)

1617-1623: Consider splitting into two separate tests for better isolation.

Two scenarios in one test can make failures harder to diagnose. The manual cleanup() call at line 1620 between renders is redundant since afterEach (line 805) already handles cleanup. Splitting into two independent tests would provide clearer failure messages and keep each test focused on a single scenario.

♻️ Proposed split into two tests
-    it('handles undefined topics/languages in grid layout', () => {
-      render(<CardDetailsPage {...defaultProps} languages={['JavaScript']} topics={undefined} />)
-      expect(screen.getByTestId('toggleable-list')).toHaveTextContent('Languages: JavaScript')
-      cleanup()
-      render(<CardDetailsPage {...defaultProps} languages={undefined} topics={['web']} />)
-      expect(screen.getByTestId('toggleable-list')).toHaveTextContent('Topics: web')
-    })
+    it('renders only languages when topics is undefined', () => {
+      render(<CardDetailsPage {...defaultProps} languages={['JavaScript']} topics={undefined} />)
+      expect(screen.getByTestId('toggleable-list')).toHaveTextContent('Languages: JavaScript')
+    })
+
+    it('renders only topics when languages is undefined', () => {
+      render(<CardDetailsPage {...defaultProps} languages={undefined} topics={['web']} />)
+      expect(screen.getByTestId('toggleable-list')).toHaveTextContent('Topics: web')
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/CardDetailsPage.test.tsx` around lines
1617 - 1623, Split the single test "handles undefined topics/languages in grid
layout" into two independent tests to improve isolation: one test rendering
<CardDetailsPage {...defaultProps} languages={['JavaScript']} topics={undefined}
/> and asserting screen.getByTestId('toggleable-list') contains "Languages:
JavaScript", and a separate test rendering <CardDetailsPage {...defaultProps}
languages={undefined} topics={['web']} /> asserting it contains "Topics: web";
remove the manual cleanup() call (it's redundant because afterEach already runs)
and keep the same test ids and expectations so only the test names and bodies
change.
frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx (2)

655-678: Redundant assigning: false override

assigning: false is already established by beforeEach and inherited through the baseMocks spread, so the explicit override on line 667 adds no value.

♻️ Suggested cleanup
     mockUseIssueMutations.mockReturnValue({
       ...baseMocks,
       assignIssue,
-      assigning: false,
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx` around lines
655 - 678, The test redundantly overrides assigning: false when baseMocks
already provides it; remove the explicit assigning: false property from the
mockUseIssueMutations return in the 'does not trigger mutations when issueId is
missing' test so the mock is returned as {...baseMocks, assignIssue} (keeping
assignIssue) and rely on the beforeEach-provided assigning value from
useIssueMutations/baseMocks.

655-678: Redundant assigning: false override

assigning: false is already established by beforeEach and inherited via the baseMocks spread, so the explicit override on line 667 is unnecessary noise. Removing it simplifies the test setup.

♻️ Suggested cleanup
     mockUseIssueMutations.mockReturnValue({
       ...baseMocks,
       assignIssue,
-      assigning: false,
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx` around lines
655 - 678, The test redundantly overrides assigning when building
mockUseIssueMutations; remove the explicit assigning: false in the
mockReturnValue call so it simply spreads baseMocks and only overrides
assignIssue (i.e., keep mockUseIssueMutations.mockReturnValue({ ...baseMocks,
assignIssue }) ), referencing assignIssue, mockUseIssueMutations, baseMocks and
assigning to locate the code to change.
frontend/__tests__/unit/components/ModuleForm.test.tsx (1)

657-687: Test title "handles selection via Set" does not match what is actually asserted

The autocomplete-select-item button fires onSelectionChange(new Set(['project-1'])), but the assertion on lines 684–686 only checks mockOnProjectChange was called with (null, 'Test'), which is the value emitted by onInputChange (the typing step), not the Set-based selection. The actual Set-selection path (which should call onProjectChange('project-1', 'Test Project 1')) is never verified.

Either update the assertion to check the project selection result or rename the test to reflect what it actually covers:

🛠️ Proposed fix — verify the Set selection callback
-      // Verify that typing triggers onProjectChange with the search term
-      await waitFor(() => {
-        expect(mockOnProjectChange).toHaveBeenCalledWith(null, 'Test')
-      })
+      // Verify that selecting via Set calls onProjectChange with the resolved project
+      await waitFor(() => {
+        expect(mockOnProjectChange).toHaveBeenCalledWith('project-1', 'Test Project 1')
+      })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx` around lines 657 -
687, The test "handles selection via Set (lines 403-404)" currently only asserts
that typing triggered onProjectChange with (null, 'Test') but never verifies the
Set-selection path; update the test to assert that after clicking the button
with data-testid 'autocomplete-select-item' (which fires onSelectionChange(new
Set(['project-1']))) the mockOnProjectChange mock is called with the selected
project id and name (e.g., mockOnProjectChange('project-1', 'Test Project 1')),
or alternatively rename the test to reflect it only verifies input change;
locate the test block using renderProjectSelector, mockQuery, the input bound to
'autocomplete-input', and the click on 'autocomplete-select-item' and add the
expectation for the Set-based selection callback (mockOnProjectChange called
with 'project-1' and 'Test Project 1').
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/__tests__/unit/components/CardDetailsPage.test.tsx`:
- Around line 2108-2139: The test assigns `name: null` on the milestone author
which violates the User type (name?: string), so update the test data used in
the `recentMilestones` array for the `CardDetailsPage`/`DetailsCardProps` case:
remove the `name` property or set it to `undefined` (instead of `null`) on the
author object so it conforms to the `User`/`Milestone` types and the test can
compile while still asserting tooltip falls back to `login`.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx`:
- Around line 400-421: The ModuleForm tests rely on advancing timers but never
enable fake timers; inside the describe('ModuleForm', ...) add
jest.useFakeTimers() in its beforeEach (the same place you call
renderModuleForm) and call jest.useRealTimers() in the corresponding afterEach
to restore real timers; this ensures jest.advanceTimersByTime(350) in the
'updates project field when ProjectSelector changes' test properly triggers the
debounce in ProjectSelector (reference the renderModuleForm helper and the test
name to locate the fix).

---

Outside diff comments:
In `@frontend/__tests__/unit/components/ModuleForm.test.tsx`:
- Line 45: The test mock's onSelectionChange signature doesn't accept null while
the component (HeroUI Autocomplete) can emit null when clearing; update the
mock/type to include null (i.e., change the onSelectionChange type to React.Key
| Set<React.Key> | 'all' | null) or adjust the mock implementation for
onSelectionChange used in ModuleForm.test.tsx to accept a null argument
(reference the onSelectionChange prop and the Clear Selection button/mock that
passes null).

---

Duplicate comments:
In `@frontend/__tests__/unit/components/CardDetailsPage.test.tsx`:
- Around line 1625-1634: The test sets details.value to null but
DetailsCardProps.details is typed as { label: string; value: string |
JSX.Element }[], causing a TypeScript error; fix by either updating the type to
allow null (e.g., include null | undefined in DetailsCardProps.details value
union) or change the test to avoid the type mismatch (remove any explicit
DetailsCardProps annotation on defaultProps so TypeScript can infer the looser
type or cast propsWithNullLeader to unknown as any before calling render);
locate the type declaration for DetailsCardProps and the test variables
defaultProps and propsWithNullLeader in CardDetailsPage.test.tsx to apply the
chosen fix.

In `@frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx`:
- Around line 545-563: Remove the fragile React.useState spy in the test for
MenteeProfilePage and instead drive the invalid status through the component's
public selection handler: update the test to trigger the existing
select-trigger-empty button (or add a hidden sibling button) to call the
component's onSelectionChange handler with new Set(['invalid_filter']), keep
mockUseQuery returning mockMenteeData, render <MenteeProfilePage />, assert both
"Open Issue 1" and "Closed Issue 1" are present, and remove the
jest.spyOn(React, 'useState') mock/restore to avoid coupling to internal initial
state.

---

Nitpick comments:
In `@frontend/__tests__/unit/components/CardDetailsPage.test.tsx`:
- Around line 1617-1623: Split the single test "handles undefined
topics/languages in grid layout" into two independent tests to improve
isolation: one test rendering <CardDetailsPage {...defaultProps}
languages={['JavaScript']} topics={undefined} /> and asserting
screen.getByTestId('toggleable-list') contains "Languages: JavaScript", and a
separate test rendering <CardDetailsPage {...defaultProps} languages={undefined}
topics={['web']} /> asserting it contains "Topics: web"; remove the manual
cleanup() call (it's redundant because afterEach already runs) and keep the same
test ids and expectations so only the test names and bodies change.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx`:
- Around line 657-687: The test "handles selection via Set (lines 403-404)"
currently only asserts that typing triggered onProjectChange with (null, 'Test')
but never verifies the Set-selection path; update the test to assert that after
clicking the button with data-testid 'autocomplete-select-item' (which fires
onSelectionChange(new Set(['project-1']))) the mockOnProjectChange mock is
called with the selected project id and name (e.g.,
mockOnProjectChange('project-1', 'Test Project 1')), or alternatively rename the
test to reflect it only verifies input change; locate the test block using
renderProjectSelector, mockQuery, the input bound to 'autocomplete-input', and
the click on 'autocomplete-select-item' and add the expectation for the
Set-based selection callback (mockOnProjectChange called with 'project-1' and
'Test Project 1').

In `@frontend/__tests__/unit/pages/EditProgram.test.tsx`:
- Around line 436-478: The jest.clearAllMocks() call inside the test "handles
null admins when access is initially allowed" is redundant because beforeEach
already calls jest.clearAllMocks(); remove the in-test call to
jest.clearAllMocks() from EditProgram.test.tsx (inside that specific test) and
keep the existing beforeEach behavior so mocks are cleared consistently; no
other changes needed.

In `@frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx`:
- Around line 655-678: The test redundantly overrides assigning: false when
baseMocks already provides it; remove the explicit assigning: false property
from the mockUseIssueMutations return in the 'does not trigger mutations when
issueId is missing' test so the mock is returned as {...baseMocks, assignIssue}
(keeping assignIssue) and rely on the beforeEach-provided assigning value from
useIssueMutations/baseMocks.
- Around line 655-678: The test redundantly overrides assigning when building
mockUseIssueMutations; remove the explicit assigning: false in the
mockReturnValue call so it simply spreads baseMocks and only overrides
assignIssue (i.e., keep mockUseIssueMutations.mockReturnValue({ ...baseMocks,
assignIssue }) ), referencing assignIssue, mockUseIssueMutations, baseMocks and
assigning to locate the code to change.

In `@frontend/__tests__/unit/pages/MyMentorship.test.tsx`:
- Around line 423-463: Move the setup of the useSearchParams mock and
globalThis.history.replaceState into the try block alongside
jest.useFakeTimers() and render so the two fake-timer tests follow the same
setup/teardown pattern; specifically, for the test case that calls
jest.useFakeTimers() and renders MyMentorshipPage, relocate the lines that call
(useSearchParams as jest.Mock).mockReturnValue(...) and
globalThis.history.replaceState(...) inside the try before render(), and keep
the existing finally block that restores history and calls jest.useRealTimers()
to ensure consistent isolation for the debounced search behavior.

In
`@frontend/src/app/my/mentorship/programs/`[programKey]/modules/[moduleKey]/edit/page.tsx:
- Around line 100-112: The handleSubmit implementation currently uses repeated
non-null assertions (formData!) when building the payload; add a single null
guard at the top of handleSubmit (e.g., if (!formData) { return; } or throw new
Error) and then remove the individual non-null assertions so the rest of the
function can safely reference formData fields (description, domains, endedAt,
experienceLevel, labels, mentorLogins, name, projectId, projectName, startedAt,
tags, etc.); update any callers/tests if they rely on throwing behavior to
instead handle the early return or thrown error.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 19 files

Confidence score: 3/5

  • Potential test failure in frontend/__tests__/unit/pages/UserDetails.test.tsx: assertion expects fallback-login but UI renders @fallback-login, which will likely break the test and block CI.
  • Score reflects a concrete, user-impacting regression risk for the test suite rather than runtime behavior; fix is straightforward but needs attention before merge.
  • Pay close attention to frontend/__tests__/unit/pages/UserDetails.test.tsx - update the assertion to match the rendered login format.
Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/__tests__/unit/pages/UserDetails.test.tsx">

<violation number="1" location="frontend/__tests__/unit/pages/UserDetails.test.tsx:1171">
P1: Test assertion expects plain 'fallback-login' text but UI renders '@fallback-login' with @ prefix. The assertion `screen.getByText('fallback-login')` will fail because UserSummary component renders the login link as `@{user?.login}` which includes the '@' prefix.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 22, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/__tests__/unit/components/ModuleForm.test.tsx (1)

45-45: ⚠️ Potential issue | 🟡 Minor

Update the Autocomplete mock type to include null

The mock's onSelectionChange type (line 45) declares React.Key | Set<React.Key> | 'all', but line 82 invokes it with null. While TypeScript doesn't flag this because test files are excluded from type checking, the mock type should be updated to match its actual usage.

Proposed fix
-    onSelectionChange?: (key: React.Key | Set<React.Key> | 'all') => void
+    onSelectionChange?: (key: React.Key | Set<React.Key> | 'all' | null) => void
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx` at line 45, The
Autocomplete mock's onSelectionChange signature doesn't allow null even though
the test invokes it with null; update the mock type for onSelectionChange to
include null (i.e., change the type from React.Key | Set<React.Key> | 'all' to
React.Key | Set<React.Key> | 'all' | null) so the mock accurately reflects usage
in the ModuleForm test where onSelectionChange is called with null; locate the
Autocomplete mock definition referencing onSelectionChange and adjust its type
accordingly.
🧹 Nitpick comments (3)
frontend/__tests__/unit/pages/UserDetails.test.tsx (2)

1129-1149: Rename test: "validates defensive check for endDate" doesn't describe what's tested.

The test body only verifies that the heatmap renders when contributionData has a single entry. The word endDate doesn't appear in any assertion, making the intent opaque to future readers. A name like "renders heatmap when contributionData has a single date entry" would be accurate.

♻️ Suggested rename
-    test('validates defensive check for endDate', async () => {
+    test('renders heatmap when contributionData has a single date entry', async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/UserDetails.test.tsx` around lines 1129 - 1149,
The test named "validates defensive check for endDate" is misleading; rename the
jest test title to something accurate like "renders heatmap when
contributionData has a single date entry" in the test that renders
<UserDetailsPage /> (the test that mocks useQuery to return
singleDateContribution and asserts getByTestId('contribution-heatmap')). Update
only the string in the test(...) call so the behavior being verified (heatmap
renders for a single contribution date) is clearly described.

1151-1173: Partial duplication with existing tests on name: null fallback.

The avatar alt-text assertion is already covered by the test at Line 1040 ("renders with login as alt text when name is null"), and the @{login} link check is covered at Line 993 ("renders with login as title when name is null"). The sole distinction here is using a different login value. Consider removing this test or folding its unique @fallback-login link assertion into one of the existing tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/UserDetails.test.tsx` around lines 1151 - 1173,
The new test "uses user login as fallback for avatar alt text and title when
name is missing" duplicates coverage from the existing tests "renders with login
as alt text when name is null" and "renders with login as title when name is
null"; remove this redundant test or merge its unique assertion into an existing
test by adjusting that test's mocked user login to 'fallback-login' (or adding
an assertion for '@fallback-login') so the avatar alt/text and the `@login` link
are both asserted in one place (look for the tests in UserDetails.test.tsx that
reference the name:null fallback and update them accordingly).
frontend/__tests__/unit/components/ModuleForm.test.tsx (1)

662-692: Test "handles selection via Set" doesn't assert the Set-selection outcome

The assertion at line 690 only verifies the typing side-effect (onProjectChange(null, 'Test')), not the result of clicking autocomplete-select-item (which fires onSelectionChange(new Set(['project-1']))). The Set-selection path — which should call onProjectChange('project-1', 'Test Project 1') — is never validated here.

Consider adding a second assertion after clicking autocomplete-select-item:

♻️ Proposed addition
     // Verify that typing triggers onProjectChange with the search term
     await waitFor(() => {
       expect(mockOnProjectChange).toHaveBeenCalledWith(null, 'Test')
     })
+    // Verify that the Set-based selection resolves to the project id + name
+    expect(mockOnProjectChange).toHaveBeenCalledWith('project-1', 'Test Project 1')
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx` around lines 662 -
692, The test "handles selection via Set (lines 403-404)" currently only asserts
the typing side-effect; after clicking the select button
(getByTestId('autocomplete-select-item')), add a second assertion that the
Set-selection path invoked onProjectChange with the selected project id and name
(e.g., expect(mockOnProjectChange).toHaveBeenCalledWith('project-1', 'Test
Project 1')). Wrap that assertion in a waitFor if needed to await async handlers
so the click's onSelectionChange(new Set(['project-1'])) outcome is validated
alongside the existing typing assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/__tests__/unit/pages/UserDetails.test.tsx`:
- Around line 1175-1196: The test for UserDetailsPage using the mock
userWithoutNameAndLogin should also assert that a null login does not render as
the string "@null"; update the test (the case that renders <UserDetailsPage />
with userWithoutNameAndLogin) to check that there is no element containing the
text "@null" (or that the login link is absent/uses the expected fallback), so
the component's rendering of @{user?.login} is validated when login is null.

---

Outside diff comments:
In `@frontend/__tests__/unit/components/ModuleForm.test.tsx`:
- Line 45: The Autocomplete mock's onSelectionChange signature doesn't allow
null even though the test invokes it with null; update the mock type for
onSelectionChange to include null (i.e., change the type from React.Key |
Set<React.Key> | 'all' to React.Key | Set<React.Key> | 'all' | null) so the mock
accurately reflects usage in the ModuleForm test where onSelectionChange is
called with null; locate the Autocomplete mock definition referencing
onSelectionChange and adjust its type accordingly.

---

Duplicate comments:
In `@frontend/__tests__/unit/components/ModuleForm.test.tsx`:
- Around line 281-289: The test now correctly adds jest.useFakeTimers() in the
setup and jest.useRealTimers() in teardown to make jest.advanceTimersByTime(350)
effective; ensure these calls are placed inside the ModuleForm describe block's
beforeEach/afterEach (where mockQuery is prepared) so any usage of
jest.advanceTimersByTime in tests that reference mockQuery or Component
rendering will actually advance timers and reset to real timers after each test.

---

Nitpick comments:
In `@frontend/__tests__/unit/components/ModuleForm.test.tsx`:
- Around line 662-692: The test "handles selection via Set (lines 403-404)"
currently only asserts the typing side-effect; after clicking the select button
(getByTestId('autocomplete-select-item')), add a second assertion that the
Set-selection path invoked onProjectChange with the selected project id and name
(e.g., expect(mockOnProjectChange).toHaveBeenCalledWith('project-1', 'Test
Project 1')). Wrap that assertion in a waitFor if needed to await async handlers
so the click's onSelectionChange(new Set(['project-1'])) outcome is validated
alongside the existing typing assertion.

In `@frontend/__tests__/unit/pages/UserDetails.test.tsx`:
- Around line 1129-1149: The test named "validates defensive check for endDate"
is misleading; rename the jest test title to something accurate like "renders
heatmap when contributionData has a single date entry" in the test that renders
<UserDetailsPage /> (the test that mocks useQuery to return
singleDateContribution and asserts getByTestId('contribution-heatmap')). Update
only the string in the test(...) call so the behavior being verified (heatmap
renders for a single contribution date) is clearly described.
- Around line 1151-1173: The new test "uses user login as fallback for avatar
alt text and title when name is missing" duplicates coverage from the existing
tests "renders with login as alt text when name is null" and "renders with login
as title when name is null"; remove this redundant test or merge its unique
assertion into an existing test by adjusting that test's mocked user login to
'fallback-login' (or adding an assertion for '@fallback-login') so the avatar
alt/text and the `@login` link are both asserted in one place (look for the tests
in UserDetails.test.tsx that reference the name:null fallback and update them
accordingly).

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 22, 2026
@sonarqubecloud
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/__tests__/unit/components/ModuleForm.test.tsx (2)

40-86: ⚠️ Potential issue | 🟡 Minor

Fix TypeScript error on line 82: null is not assignable to the mock's onSelectionChange type union

The mock's onSelectionChange prop at line 45 is typed as:

onSelectionChange?: (key: React.Key | Set<React.Key> | 'all') => void

Calling it with null at line 82 causes a TypeScript error since null is not in the union. The mock accepts a custom set of possible values (including Set<React.Key> and 'all') for testing purposes. Add | null to the union to support the clear action test case:

-    onSelectionChange?: (key: React.Key | Set<React.Key> | 'all') => void
+    onSelectionChange?: (key: React.Key | Set<React.Key> | 'all' | null) => void
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx` around lines 40 - 86,
The mock's onSelectionChange prop signature doesn't allow null, causing the
TypeScript error when Clear Selection calls onSelectionChange(null); update the
prop type in the test component (the onSelectionChange?: (key: React.Key |
Set<React.Key> | 'all') => void declaration) to include null by changing the
union to React.Key | Set<React.Key> | 'all' | null so the Clear Selection button
call onSelectionChange?.(null) is valid.

40-86: ⚠️ Potential issue | 🟡 Minor

Add null to mock onSelectionChange type union

Line 45 types onSelectionChange as (key: React.Key | Set<React.Key> | 'all') => void, but line 82 passes null. This is a TypeScript error. HeroUI Autocomplete's onSelectionChange callback receives null when a selection is cleared, so the mock's type must include it.

Fix
-    onSelectionChange?: (key: React.Key | Set<React.Key> | 'all') => void
+    onSelectionChange?: (key: React.Key | Set<React.Key> | 'all' | null) => void
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx` around lines 40 - 86,
The mock prop type for onSelectionChange is missing null; update its type union
in the mock props declaration (the anonymous component props where
onSelectionChange is declared) to include null — i.e. change onSelectionChange?:
(key: React.Key | Set<React.Key> | 'all' | null) => void — so the existing Clear
Selection button that calls onSelectionChange?.(null) is type-safe (refer to the
autocomplete mock props and the onSelectionChange usage in the Clear Selection
button).
🧹 Nitpick comments (3)
frontend/__tests__/unit/pages/SnapshotDetails.test.tsx (1)

380-403: Strengthen the first test's assertions and clarify the test name.

The test name "uses fallback key" refers to a React list key prop, which isn't directly observable in DOM assertions — the test can't actually verify that behavior. More practically, the test only asserts v0.9.2 is rendered, but since repositoryName is still defined ('test-project-1'), there's no assertion differentiating it from the second test. Without a negative assertion for 'Unknown repository', both the "no id" and the "no id + no repositoryName" cases look identical from this test's perspective.

✨ Proposed improvements
-  test('renders release without id (uses fallback key)', async () => {
+  test('renders release without id (repositoryName still displayed)', async () => {
     const releaseWithoutId = {
       ...mockSnapshotDetailsData.snapshot.newReleases[0],
       id: undefined,
     }
     ;(useQuery as unknown as jest.Mock).mockReturnValue({
       data: {
         snapshot: {
           ...mockSnapshotDetailsData.snapshot,
           newReleases: [releaseWithoutId],
           newChapters: [],
           newProjects: [],
         },
       },
       error: null,
       loading: false,
     })

     render(<SnapshotDetailsPage />)

     await waitFor(() => {
       expect(screen.getByText('v0.9.2')).toBeInTheDocument()
+      expect(screen.getByText('test-project-1')).toBeInTheDocument()
+      expect(screen.queryByText('Unknown repository')).not.toBeInTheDocument()
     })
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/SnapshotDetails.test.tsx` around lines 380 -
403, Rename and strengthen the test for the "no id" release case in
SnapshotDetails.test.tsx: update the test name from "renders release without id
(uses fallback key)" to something like "renders release without id and shows
repository name or fallback" and modify the mocked release object (currently
derived from mockSnapshotDetailsData.snapshot.newReleases[0] with id: undefined)
to also remove repositoryName in a separate test case; in the first test assert
that the known repositoryName (e.g., 'test-project-1') is rendered and assert
that 'Unknown repository' is not present, and add a second test where you set
repositoryName: undefined and assert the fallback text 'Unknown repository'
appears; keep using SnapshotDetailsPage, useQuery mock, and
mockSnapshotDetailsData to locate where to change the mocks and expectations.
frontend/__tests__/unit/pages/UserDetails.test.tsx (1)

1129-1149: Misleading test name.

'validates defensive check for endDate' doesn't describe what's actually being exercised. The test verifies that a single-date contributionData entry is sufficient to render the heatmap; the name should reflect that.

📝 Suggested rename
-    test('validates defensive check for endDate', async () => {
+    test('renders heatmap when contributionData contains a single date entry', async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/unit/pages/UserDetails.test.tsx` around lines 1129 - 1149,
The test name 'validates defensive check for endDate' is misleading; rename the
test to reflect that it ensures a heatmap renders when contributionData contains
a single date. Update the test declaration string in the test block that creates
singleDateContribution (using mockUserDetailsData and mocking useQuery to return
it) — change the test(...) first argument to something like 'renders heatmap
when contributionData has a single date' while leaving the rest of the test
(render(<UserDetailsPage />), waitFor, and assertions) unchanged.
frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/edit/page.tsx (1)

102-127: Consider retaining a defensive formData null guard at the top of handleSubmit.

The non-null assertions (formData!) are safe today because the form is only rendered—and therefore handleSubmit only callable—when formData is non-null (guarded at lines 175-177). However, the assertions silently bypass TypeScript's null-safety: if the render guard is ever moved or the component is restructured, these will become runtime crashes with no compile-time warning.

A one-line early return is cheaper than a future debugging session:

♻️ Proposed defensive guard
 const handleSubmit = async (e: React.FormEvent) => {
   e.preventDefault()
+  if (!formData) return

   try {
     const currentUserLogin = sessionData?.user?.login
     ...
     const input: UpdateModuleInput = {
-      description: formData!.description,
-      domains: parseCommaSeparated(formData!.domains),
-      endedAt: formData!.endedAt || '',
-      experienceLevel: formData!.experienceLevel as ExperienceLevelEnum,
+      description: formData.description,
+      domains: parseCommaSeparated(formData.domains),
+      endedAt: formData.endedAt || '',
+      experienceLevel: formData.experienceLevel as ExperienceLevelEnum,
       key: moduleKey,
-      labels: parseCommaSeparated(formData!.labels),
-      name: formData!.name,
+      labels: parseCommaSeparated(formData.labels),
+      name: formData.name,
       programKey: programKey,
-      projectId: formData!.projectId,
-      projectName: formData!.projectName,
-      startedAt: formData!.startedAt || '',
-      tags: parseCommaSeparated(formData!.tags),
+      projectId: formData.projectId,
+      projectName: formData.projectName,
+      startedAt: formData.startedAt || '',
+      tags: parseCommaSeparated(formData.tags),
     }

     if (isAdmin) {
-      input.mentorLogins = parseCommaSeparated(formData!.mentorLogins)
+      input.mentorLogins = parseCommaSeparated(formData.mentorLogins)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/my/mentorship/programs/`[programKey]/modules/[moduleKey]/edit/page.tsx
around lines 102 - 127, The handleSubmit function uses many non-null assertions
on formData (formData!) which can cause runtime crashes if formData becomes
null; add a one-line defensive guard at the start of handleSubmit that returns
early when formData is null/undefined (e.g. if (!formData) return) before
building the UpdateModuleInput and calling parseCommaSeparated for mentorLogins,
so all references to formData inside handleSubmit are safe; update any dependent
logic in handleSubmit (e.g., building input, isAdmin check) to assume formData
exists after the guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@frontend/__tests__/unit/components/ModuleForm.test.tsx`:
- Around line 40-86: The mock's onSelectionChange prop signature doesn't allow
null, causing the TypeScript error when Clear Selection calls
onSelectionChange(null); update the prop type in the test component (the
onSelectionChange?: (key: React.Key | Set<React.Key> | 'all') => void
declaration) to include null by changing the union to React.Key | Set<React.Key>
| 'all' | null so the Clear Selection button call onSelectionChange?.(null) is
valid.
- Around line 40-86: The mock prop type for onSelectionChange is missing null;
update its type union in the mock props declaration (the anonymous component
props where onSelectionChange is declared) to include null — i.e. change
onSelectionChange?: (key: React.Key | Set<React.Key> | 'all' | null) => void —
so the existing Clear Selection button that calls onSelectionChange?.(null) is
type-safe (refer to the autocomplete mock props and the onSelectionChange usage
in the Clear Selection button).

---

Duplicate comments:
In `@frontend/__tests__/unit/components/CardDetailsPage.test.tsx`:
- Around line 1629-1638: The test passes value: null into CardDetailsPage but
DetailsCardProps.details expects value: string | JSX.Element, causing a
TypeScript error; fix the test by making the null compatible (e.g., change the
test's propsWithNullLeader to cast the value: null as any or as unknown as
string | JSX.Element), or remove any explicit DetailsCardProps annotation on
defaultProps so inference allows null — locate the test variables (defaultProps,
propsWithNullLeader) and update the value assignment accordingly.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx`:
- Around line 279-289: Tests already properly scope fake timers: ensure the
beforeEach contains jest.useFakeTimers(), jest.clearAllMocks(), and
mockQuery.mockResolvedValue(...) and the afterEach restores timers with
jest.useRealTimers(); keep these calls (beforeEach, afterEach,
jest.useFakeTimers, jest.useRealTimers, and any jest.advanceTimersByTime(...)
usage in tests) as-is so timer-dependent tests remain deterministic.
- Around line 279-289: The beforeEach/afterEach setup correctly scopes fake
timers for the ModuleForm tests; keep the jest.useFakeTimers() call inside the
beforeEach and jest.useRealTimers() in afterEach so
jest.advanceTimersByTime(350) remains deterministic, and ensure you do not add
global fake-timer calls outside this describe block (check the beforeEach,
afterEach, jest.useFakeTimers, jest.useRealTimers, and tests that call
jest.advanceTimersByTime to confirm no conflicting timer setup).

In `@frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx`:
- Around line 545-563: Replace the fragile React.useState spy by driving the
component via the mock Select: update the Select mock (the component exposing
"select-trigger-empty") to add a hidden button that calls its onSelectionChange
handler with new Set(['invalid_filter']), then change the test case in
MenteeProfilePage.test.tsx to render the page and programmatically click that
hidden button (instead of mocking React.useState) to trigger the invalid filter
path; keep mockUseQuery return as-is and retain the try/finally cleanup around
mocks.

In `@frontend/__tests__/unit/pages/UserDetails.test.tsx`:
- Around line 1175-1197: The test "uses \"User Avatar\" fallback when both name
and login are missing" in UserDetails.test.tsx now includes the missing
assertion expect(screen.queryByText('@null')).not.toBeInTheDocument(), so no
code changes are required; leave the test as-is (refer to the test function and
the added assertion line) and mark the previous review request as resolved.

In
`@frontend/src/app/my/mentorship/programs/`[programKey]/modules/[moduleKey]/issues/page.tsx:
- Around line 117-120: The change tightens the guard in onSelectionChange to
only block undefined keys which is correct; keep the updated code in
onSelectionChange that converts the selected keys to a single key (const [key] =
Array.from(keys as Set<string>)) and use if (key !== undefined)
handleLabelChange(key) so empty-string keys (if ever present) are allowed while
preventing the undefined-from-empty-Set bug—no further code changes needed to
functions onSelectionChange or handleLabelChange.

---

Nitpick comments:
In `@frontend/__tests__/unit/pages/SnapshotDetails.test.tsx`:
- Around line 380-403: Rename and strengthen the test for the "no id" release
case in SnapshotDetails.test.tsx: update the test name from "renders release
without id (uses fallback key)" to something like "renders release without id
and shows repository name or fallback" and modify the mocked release object
(currently derived from mockSnapshotDetailsData.snapshot.newReleases[0] with id:
undefined) to also remove repositoryName in a separate test case; in the first
test assert that the known repositoryName (e.g., 'test-project-1') is rendered
and assert that 'Unknown repository' is not present, and add a second test where
you set repositoryName: undefined and assert the fallback text 'Unknown
repository' appears; keep using SnapshotDetailsPage, useQuery mock, and
mockSnapshotDetailsData to locate where to change the mocks and expectations.

In `@frontend/__tests__/unit/pages/UserDetails.test.tsx`:
- Around line 1129-1149: The test name 'validates defensive check for endDate'
is misleading; rename the test to reflect that it ensures a heatmap renders when
contributionData contains a single date. Update the test declaration string in
the test block that creates singleDateContribution (using mockUserDetailsData
and mocking useQuery to return it) — change the test(...) first argument to
something like 'renders heatmap when contributionData has a single date' while
leaving the rest of the test (render(<UserDetailsPage />), waitFor, and
assertions) unchanged.

In
`@frontend/src/app/my/mentorship/programs/`[programKey]/modules/[moduleKey]/edit/page.tsx:
- Around line 102-127: The handleSubmit function uses many non-null assertions
on formData (formData!) which can cause runtime crashes if formData becomes
null; add a one-line defensive guard at the start of handleSubmit that returns
early when formData is null/undefined (e.g. if (!formData) return) before
building the UpdateModuleInput and calling parseCommaSeparated for mentorLogins,
so all references to formData inside handleSubmit are safe; update any dependent
logic in handleSubmit (e.g., building input, isAdmin check) to assume formData
exists after the guard.

@HarshitVerma109
Copy link
Contributor Author

@kasya, please review it and let me know if any changes are required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve Frontend Unit Test Coverage to 99%+

2 participants