Skip to content

Comments

Improve frontend test coverage above 80% and add missing test files#3864

Merged
kasya merged 9 commits intoOWASP:mainfrom
anurag2787:fix/frontend-test
Feb 10, 2026
Merged

Improve frontend test coverage above 80% and add missing test files#3864
kasya merged 9 commits intoOWASP:mainfrom
anurag2787:fix/frontend-test

Conversation

@anurag2787
Copy link
Contributor

@anurag2787 anurag2787 commented Feb 8, 2026

Proposed change

This PR improves frontend unit test coverage for files that were previously below the 80% line coverage threshold. It also adds new test files for components that previously had no test coverage.

Resolves #3810

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 8, 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

Walkthrough

Adds and expands many frontend unit tests across components, pages, and utils; introduces extensive mocks and interaction assertions (keyboard, hover, pagination, form flows). One small production change: RecentPullRequests onClick now uses repositoryName directly (removed empty-string fallback).

Changes

Cohort / File(s) Summary
Interaction & icon tests
frontend/__tests__/unit/components/CalendarButton.test.tsx, frontend/__tests__/unit/components/MetricsPDFButton.test.tsx, frontend/__tests__/unit/components/MentorshipPullRequest.test.tsx, frontend/__tests__/unit/components/Release.test.tsx, frontend/__tests__/unit/components/ContributorAvatar.test.tsx
Replaced long-title overflow tests with hover/icon-state assertions; added keyboard and tooltip/aria fallback tests; status-badge and avatar edge-case coverage; PDF download click behavior tested.
Pages, wrappers & lifecycle tests
frontend/__tests__/unit/components/CardDetailsPage.test.tsx, frontend/__tests__/unit/components/FontLoaderWrapper.test.tsx, frontend/__tests__/unit/components/DashboardWrapper.test.tsx, frontend/__tests__/unit/components/ChapterMapWrapper.test.tsx, frontend/__tests__/unit/components/StructuredDataScript.test.tsx, frontend/__tests__/unit/components/ScrollToTop.test.tsx
Large suites with heavy mocks for auth/navigation, font loading, geolocation/map sharing, structured-data sanitization, scroll visibility and timeout cleanup; many interaction/rendering scenarios added.
Modules & forms tests
frontend/__tests__/unit/components/ModuleCard.test.tsx, frontend/__tests__/unit/components/ModuleForm.test.tsx, frontend/__tests__/unit/components/SingleModuleCard.test.tsx, frontend/__tests__/unit/components/forms/shared/FormButtons.test.tsx, frontend/__tests__/unit/components/forms/shared/FormContainer.test.tsx, frontend/__tests__/unit/pages/ProgramForm.test.tsx
Comprehensive tests for module cards and forms: input updates, project selector/autocomplete, validation, submission flows, keyboard accessibility, duration helper tests, and name-uniqueness checks.
Lists, navigation & accessibility tests
frontend/__tests__/unit/components/ToggleableList.test.tsx, frontend/__tests__/unit/components/RepositoryCard.test.tsx, frontend/__tests__/unit/components/ProgramCard.test.tsx, frontend/__tests__/unit/components/SortBy.test.tsx, frontend/__tests__/unit/components/Search.test.tsx, frontend/__tests__/unit/components/Leaders.test.tsx, frontend/__tests__/unit/components/UserMenu.test.tsx, frontend/__tests__/unit/components/SkeletonBase.test.tsx
Added keyboard navigation tests, disabled-state handling, outside-click/menu-close flows, aria/keyboard behaviors, and expanded skeleton branches.
Page-level tests — routing, data & edge cases
frontend/__tests__/unit/pages/... (About.test.tsx, Chapters.test.tsx, Contribute.test.tsx, CreateModule.test.tsx, EditModule.test.tsx, EditProgram.test.tsx, Header.test.tsx, IssuesPage.test.tsx, MenteeProfilePage.test.tsx, ModuleIssueDetailsPage.test.tsx, MyMentorship.test.tsx, NotFound.test.tsx, Organization.test.tsx, ProgramDetails.test.tsx, ProjectHealthDashboardMetricsDetails.test.tsx, ProjectsHealthDashboardMetrics.test.tsx, UserDetails.test.tsx)
Expanded page tests for milestone states, search-query selection, modals, access control/loading/redirects, header resize/outside-click, label extraction, issue link handling, PR pagination, metrics filters/pagination/sorting, and user-data fallbacks.
Utilities & validation
frontend/__tests__/unit/utils/formValidationUtils.test.ts
New comprehensive tests for form validation utilities: required/name/description/start/end validators and the validation-rule builder.
Other component edge cases
frontend/__tests__/unit/components/ContributionHeatmap.test.tsx, frontend/__tests__/unit/components/TruncatedText.test.tsx, frontend/__tests__/unit/components/MultiSearch.test.tsx, frontend/__tests__/unit/components/ToggleableList.test.tsx
Added date-range and swap handling for ContributionHeatmap; ResizeObserver mocking and lifecycle tests for TruncatedText; debounce import adjustment in MultiSearch tests; expanded interaction coverage.
Production change
frontend/src/components/RecentPullRequests.tsx
onClick navigation now builds repository URL using repositoryName directly (removed `

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • arkid15r
  • kasya
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes comprehensively address issue #3810 by adding tests for all listed files requiring coverage improvements, including 0%, <60%, and 60-79% coverage targets.
Out of Scope Changes check ✅ Passed All changes are in-scope test additions. One minor production code change (RecentPullRequests.tsx) fixes a navigation bug directly related to test quality but remains minimal and focused.
Description check ✅ Passed The PR description accurately describes the changeset as improving frontend unit test coverage above 80% and adding missing test files, which directly aligns with the raw summary showing extensive test additions across multiple components and pages.
Title check ✅ Passed The title clearly summarizes the main objective of the PR: improving frontend test coverage above 80% and adding missing test files, which is fully reflected in the changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 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

@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.

20 issues found across 50 files

Confidence score: 4/5

  • Most issues are test assertion problems (e.g., always-true checks or unconfigured mocks) in frontend/__tests__/unit/components/ProgramForm.test.tsx and related files, which reduce test reliability but don’t indicate runtime regressions.
  • The highest-severity items are incorrect or ineffective assertions in unit tests (severity 5–6/10), suggesting some tests may pass without actually validating behavior.
  • This looks safe to merge with minimal risk since the issues are confined to test quality rather than production code.
  • Pay close attention to frontend/__tests__/unit/components/ProgramForm.test.tsx, frontend/__tests__/unit/components/DashboardWrapper.test.tsx, frontend/__tests__/unit/pages/ProjectsHealthDashboardMetrics.test.tsx, frontend/__tests__/unit/components/SingleModuleCard.test.tsx, frontend/__tests__/unit/pages/CreateModule.test.tsx, frontend/__tests__/unit/components/ModuleForm.test.tsx - improve assertions to ensure tests verify intended behavior.
Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="frontend/__tests__/unit/utils/formValidationUtils.test.ts">

<violation number="1" location="frontend/__tests__/unit/utils/formValidationUtils.test.ts:20">
P3: Redundant test case: this test is functionally identical to line 13-15 ('returns error message when value is empty string'). Both test `validateRequired('', ...)` with an empty string. The description 'null-like empty' is misleading since no null value is being tested. Consider removing this duplicate test or actually testing null handling if that's the intent.</violation>
</file>

<file name="frontend/__tests__/unit/components/SingleModuleCard.test.tsx">

<violation number="1" location="frontend/__tests__/unit/components/SingleModuleCard.test.tsx:445">
P2: These tests have misleading names - they claim to verify EntityActions rendering behavior for admin/non-admin users, but the assertions only check that 'Test Module' text is present, which doesn't verify EntityActions at all. Consider either mocking EntityActions and asserting it's rendered/not rendered, or renaming the tests to accurately describe what they verify (e.g., 'renders without error when user is admin').</violation>
</file>

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

<violation number="1" location="frontend/__tests__/unit/pages/CreateModule.test.tsx:231">
P2: Test does not verify what its name claims. The test 'renders form with min and max dates when program has start and end dates' only checks that 'Create New Module' text exists, but doesn't actually verify that date inputs have the correct min/max constraints from the program dates. Consider adding assertions that verify the date input elements have the expected `min` and `max` attributes, or rename the test to accurately reflect what it tests.</violation>
</file>

<file name="frontend/__tests__/unit/components/DashboardWrapper.test.tsx">

<violation number="1" location="frontend/__tests__/unit/components/DashboardWrapper.test.tsx:138">
P2: The mock for `useDjangoSession` is configured AFTER the initial render, causing the first render to use an unconfigured mock (undefined). Move the mock setup before the `render()` call to ensure consistent test behavior.</violation>
</file>

<file name="frontend/__tests__/unit/components/CalendarButton.test.tsx">

<violation number="1" location="frontend/__tests__/unit/components/CalendarButton.test.tsx:343">
P2: This mouse-leave test doesn't verify the icon reverted; it only checks the button still exists. Assert that the icon markup returns to the initial state after mouseLeave.</violation>

<violation number="2" location="frontend/__tests__/unit/components/CalendarButton.test.tsx:343">
P2: This hover test never asserts the icon change; it only re-checks that the button exists, so the test will pass even if the hover state is broken. Add an assertion that the rendered icon changes after hover.</violation>
</file>

<file name="frontend/__tests__/unit/components/ProgramCard.test.tsx">

<violation number="1" location="frontend/__tests__/unit/components/ProgramCard.test.tsx:435">
P2: This test doesn’t assert that `stopPropagation` was called, so it doesn’t validate the behavior it claims to cover. Add an expectation on the mock event to ensure the handler is exercised.</violation>
</file>

<file name="frontend/__tests__/unit/components/ModuleForm.test.tsx">

<violation number="1" location="frontend/__tests__/unit/components/ModuleForm.test.tsx:421">
P2: Test 'handles single key selection' has no assertions. It performs actions (triggering search and clicking select) but never verifies the expected outcome. Tests without assertions will always pass.</violation>

<violation number="2" location="frontend/__tests__/unit/components/ModuleForm.test.tsx:477">
P2: Test 'sets all fields as touched on submit' has no assertion after form submission. The test description promises to verify fields are touched, but no actual assertion validates this behavior. Consider adding assertions to verify the touched state is set on form fields.</violation>

<violation number="3" location="frontend/__tests__/unit/components/ModuleForm.test.tsx:617">
P2: Test 'handles selection via Set' lacks meaningful assertions. The test performs actions but ends with a comment acknowledging the test doesn't properly verify behavior. Tests without assertions will always pass and provide no value.</violation>

<violation number="4" location="frontend/__tests__/unit/components/ModuleForm.test.tsx:630">
P2: Test 'handles "all" key selection' has a trivial assertion that only verifies the button still exists in the DOM. This doesn't test the actual handling of 'all' selection. Consider asserting the expected state after selection.</violation>

<violation number="5" location="frontend/__tests__/unit/components/ModuleForm.test.tsx:796">
P2: Test 'filters out currently selected project from suggestions' doesn't verify filtering behavior. It only asserts `mockQuery` was called but never checks that project-1 was actually filtered from results. Consider adding an assertion for the filtering logic.</violation>
</file>

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

<violation number="1" location="frontend/__tests__/unit/pages/MyMentorship.test.tsx:264">
P2: Reset globalThis.scrollTo after this test (or in afterEach) to avoid leaking the mock into other tests and causing cross-test interference.</violation>
</file>

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

<violation number="1" location="frontend/__tests__/unit/pages/ProjectsHealthDashboardMetrics.test.tsx:208">
P2: This test's assertion doesn't verify the filter was applied - the button was already in the document before clicking. Consider asserting that the filter action actually occurred (e.g., verify URL params changed, query variables updated, or UI state reflects the active filter).

This same issue affects multiple tests in this file: all health filters, level filters, sort options, and reset buttons.</violation>
</file>

<file name="frontend/__tests__/unit/components/ProgramForm.test.tsx">

<violation number="1" location="frontend/__tests__/unit/components/ProgramForm.test.tsx:188">
P2: This assertion always passes since `array.length` is always >= 0. The test named 'enables submit button when not loading' doesn't actually verify that the button is enabled. Consider asserting that the button exists and is not disabled.</violation>

<violation number="2" location="frontend/__tests__/unit/components/ProgramForm.test.tsx:188">
P2: This assertion always passes since `array.length` is always >= 0. The test named 'disables submit button when loading' doesn't actually verify that the button is disabled. Consider asserting that the button exists and has a `disabled` attribute.</violation>

<violation number="3" location="frontend/__tests__/unit/components/ProgramForm.test.tsx:241">
P2: This assertion always passes since `array.length` is always >= 0. The test named 'calls setFormData when mentees limit changes' doesn't actually verify the stated behavior. Consider finding the mentees limit input and testing the onChange handler.</violation>

<violation number="4" location="frontend/__tests__/unit/components/ProgramForm.test.tsx:820">
P2: Incorrect assertion: `queryByText` returns `null` (not `undefined`) when an element is not found, so `.not.toBeUndefined()` will always pass. Use `.toBeInTheDocument()` instead.</violation>

<violation number="5" location="frontend/__tests__/unit/components/ProgramForm.test.tsx:837">
P2: Incorrect assertion: `queryByText` returns `null` (not `undefined`) when an element is not found, so `.not.toBeUndefined()` will always pass. Use `.toBeInTheDocument()` instead.</violation>
</file>

<file name="frontend/__tests__/unit/components/ModuleCard.test.tsx">

<violation number="1" location="frontend/__tests__/unit/components/ModuleCard.test.tsx:729">
P2: Test has uncertain expected behavior and uses a loose assertion. The comment 'depending on implementation' indicates this test doesn't verify a specific expected outcome. Tests should document and assert known expected behavior - either determine what `getSimpleDuration` should return for negative durations and assert that specific value, or handle this edge case explicitly in the implementation.</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.

Actionable comments posted: 7

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

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

170-174: ⚠️ Potential issue | 🟡 Minor

The "fallback" test doesn't actually test the fallback path.

The component uses event.title || 'event' as the fallback for empty/missing titles. This test passes title: 'Untitled' which is a truthy string, so it exercises the normal path, not the fallback. To test the actual fallback, pass title: '' or title: undefined and assert the aria-label is 'Add event to Calendar'.

Proposed fix
     it('uses fallback for events without explicit title', () => {
-      render(<CalendarButton event={{ ...mockEvent, title: 'Untitled' }} />)
+      render(<CalendarButton event={{ ...mockEvent, title: '' }} />)
       const button = screen.getByRole('button')
-      expect(button).toHaveAttribute('aria-label', 'Add Untitled to Calendar')
+      expect(button).toHaveAttribute('aria-label', 'Add event to Calendar')
     })
frontend/src/components/RecentPullRequests.tsx (1)

35-45: ⚠️ Potential issue | 🟠 Major

Extend guard to also check organizationName to prevent malformed navigation URLs.

Line 35 guards only item?.repositoryName, but line 43 interpolates both item.organizationName and item.repositoryName. Since both fields are optional (?: in the PullRequest type), if organizationName is missing or falsy, the router will push to /organizations/null/repositories/....

Proposed fix
-        {item?.repositoryName && (
+        {item?.organizationName && item?.repositoryName && (
frontend/__tests__/unit/pages/Header.test.tsx (1)

484-499: ⚠️ Potential issue | 🟡 Minor

No-op test: resize event never reaches the component handler.

window.addEventListener is replaced with jest.fn() in beforeEach (line 189), so globalThis.dispatchEvent(new Event('resize')) at line 495 never triggers the component's registered resize callback. The only assertion is expect(true).toBe(true), making this test vacuous — it will always pass regardless of component behavior.

The dedicated resize tests in the "Resize Handler" describe block (lines 741–824) already cover this behavior properly by extracting and invoking the handler directly. Consider removing this test to avoid a false sense of coverage.

🤖 Fix all issues with AI agents
In `@frontend/__tests__/unit/components/CalendarButton.test.tsx`:
- Around line 335-363: The two hover tests in CalendarButton.test.tsx don't
assert the icon change; update each test (the 'toggles icon on hover - shows
FaCalendarPlus when hovering' and 'reverts to FaCalendar icon when mouse
leaves') to capture the button's rendered SVG before and after the mouse events
and assert they differ (or match expected values). Specifically, within the
tests that render <CalendarButton event={mockEvent} /> find the button
(screen.getByRole('button')), locate the contained <svg> (e.g.,
button.querySelector('svg')) and store its innerHTML or the SVG <path> 'd'
attribute, then fireEvent.mouseEnter(mouseLeave) and await waitFor to assert
that the post-event svg.innerHTML (or path 'd') is different for the hover test
(and equals the original for the mouse leave test); if needed, add a data-testid
on the SVG in the CalendarButton component to make selection reliable. Ensure
the assertions check the actual SVG content rather than only
toBeInTheDocument().

In `@frontend/__tests__/unit/components/DashboardWrapper.test.tsx`:
- Around line 138-175: The test currently calls render() before configuring the
mockUseDjangoSession return value, causing order-dependent behavior; fix by
setting mockUseDjangoSession.mockReturnValue(...) with the initial syncing state
before the first render of <DashboardWrapper> (or alternatively add
jest.resetAllMocks() in the test suite beforeEach), then proceed to rerender and
update the mock for the "after sync completes" state — target the
mockUseDjangoSession mock and the DashboardWrapper render/rerender calls to
reorder the setup so the initial render uses the intended mocked return value.

In `@frontend/__tests__/unit/components/ProgramCard.test.tsx`:
- Around line 422-442: The test currently doesn't verify stopPropagation because
passing a plain object to fireEvent.mouseDown won't replace
Event.prototype.stopPropagation and no assertion checks it; update the test
"stops propagation on mousedown for actions wrapper" to create a real MouseEvent
(e.g. new MouseEvent('mousedown', { bubbles: true })) or spy on
Event.prototype.stopPropagation, dispatch the event against the actionsWrapper
(using actionsWrapper!.dispatchEvent(event) or fireEvent(actionsWrapper!,
event)), assert that the spy was called (expect(spy).toHaveBeenCalled()), and
restore the spy after the test; reference the actionsWrapper variable and
ProgramCard test block so the change is applied in that spec.

In `@frontend/__tests__/unit/components/SingleModuleCard.test.tsx`:
- Around line 444-496: The tests under the "Admin Access and EntityActions"
describe block are only asserting the module title and never verify
EntityActions; mock the EntityActions component (similar to ShowMoreButton) to
return a predictable element (e.g., a span with text "MockedEntityActions") and
then update the three tests that call render(<SingleModuleCard ... />) to assert
presence (expect(...toBeInTheDocument())) of that mocked marker when accessLevel
is "admin" and session user is an admin, and assert absence
(expect(queryByText(...)).toBeNull() or .not.toBeInTheDocument()) when the user
is not admin or accessLevel is not "admin"; keep using mockUseSession and the
existing test names to locate where to change mocks and assertions.

In `@frontend/__tests__/unit/components/SkeletonBase.test.tsx`:
- Around line 144-152: Update the assertion in the SkeletonBase unit test so it
verifies all responsive grid classes output by the userCardRender used for both
users and organizations; in the test for SkeletonBase (component SkeletonBase,
test file SkeletonBase.test.tsx) replace the current
expect(gridContainer).toHaveClass(...) call to include the six classes 'grid',
'grid-cols-1', 'gap-6', 'sm:grid-cols-2', 'lg:grid-cols-3', and 'xl:grid-cols-4'
so the test accurately reflects the component's actual output.

In `@frontend/__tests__/unit/pages/About.test.tsx`:
- Around line 610-627: The mock response merges ...mockAboutData which yields {
project, topContributors, users } but the component under test expects
leader1/leader2/leader3 instead of users; update the three tests that build the
mocked data (the one setting project.recentMilestones and the two sibling tests)
to construct the shape explicitly: keep project with recentMilestones as you
have, preserve topContributors, and add leader1, leader2, leader3 properties
(copied from the appropriate entries in mockAboutData or created from
mockAboutData.topContributors) while removing the unexpected users field so the
mocked data matches the component's expected GraphQL response shape.

In `@frontend/__tests__/unit/pages/ProjectsHealthDashboardMetrics.test.tsx`:
- Around line 213-265: The tests currently assert only that the clicked button
is in the document which is tautological; update each health-filter test (the
ones that render <MetricsPage /> and use getByRole + fireEvent) to assert an
observable side-effect instead of the button presence. For example, after
clicking the "Healthy"/"Need Attention"/"Unhealthy" button assert the mocked
useQuery was invoked with the expected variables (e.g.
expect(useQuery).toHaveBeenLastCalledWith(expect.objectContaining({ variables:
expect.objectContaining({ level: 'healthy' }) }))), or assert
router.replace/push was called with the expected query, or assert the rendered
metric cards change to the expected filtered content from mockHealthMetricsData;
replace the tautological waitFor(expect(button).toBeInTheDocument()) with one of
these meaningful assertions referencing useQuery, router.replace/router.push, or
the metric card elements.
🟡 Minor comments (21)
frontend/__tests__/unit/pages/Contribute.test.tsx-224-226 (1)

224-226: ⚠️ Potential issue | 🟡 Minor

Wrap post-close assertion in waitFor to avoid flakiness.

Closing the modal triggers a state update; the "Read More" button may not reappear synchronously. Every other assertion in this file that follows a user interaction is wrapped in waitFor — this one should be consistent.

Proposed fix
-    // After closing, the Read More button should be visible again
-    expect(screen.getByText('Read More')).toBeInTheDocument()
+    // After closing, the Read More button should be visible again
+    await waitFor(() => {
+      expect(screen.getByText('Read More')).toBeInTheDocument()
+    })
frontend/__tests__/unit/pages/MyMentorship.test.tsx-280-310 (1)

280-310: ⚠️ Potential issue | 🟡 Minor

Search callback test has no meaningful assertion.

screen.getByTestId('search-input') already throws if the element is missing, so the expect(searchInput).toBeInTheDocument() on line 309 is redundant and tests nothing about the search behavior. The test fires a change event but never verifies any side effect (e.g., that useQuery was re-invoked with updated variables, or that the rendered output changed).

Either add a meaningful assertion — such as verifying that the query was called with the search term — or remove this test to avoid false confidence in coverage. Based on learnings, page-level tests should focus on submission handling and navigation logic, not just element presence.

frontend/__tests__/unit/pages/EditModule.test.tsx-257-263 (1)

257-263: ⚠️ Potential issue | 🟡 Minor

Test doesn't actually verify graceful error handling.

The test name says "handles form submission error gracefully," but the only assertion after clicking Save is that the mutation was called. There's no verification that:

  • An error toast is shown, or
  • The user is not redirected (i.e., mockPush was not called), or
  • Any error UI is rendered.

Without these, the test only confirms the mutation fires — not that the error path is handled. Consider adding at least:

     await waitFor(() => {
       expect(mockUpdateModule).toHaveBeenCalled()
     })
+
+    // Verify no navigation occurred on error
+    expect(mockPush).not.toHaveBeenCalled()
frontend/__tests__/unit/components/FontLoaderWrapper.test.tsx-138-160 (1)

138-160: ⚠️ Potential issue | 🟡 Minor

Biome noThenProperty lint violation — restructure the mock to use a real Promise.

Line 147 assigns then as a plain object property, which Biome rightfully flags because any object with a then method is treated as a thenable by the JS runtime, leading to subtle issues. Since you're mocking document.fonts.ready, you can use an actual resolved promise and spy on its then instead:

Proposed fix
   it('calls document.fonts.ready.then() on mount', () => {
-    const thenSpy = jest.fn((callback: () => void) => {
-      callback()
-      return Promise.resolve()
-    })
-
-    Object.defineProperty(document, 'fonts', {
-      value: {
-        ready: {
-          then: thenSpy,
-        },
-      },
-      configurable: true,
-    })
+    const fontsReadyPromise = Promise.resolve()
+    const thenSpy = jest.spyOn(fontsReadyPromise, 'then')
+
+    Object.defineProperty(document, 'fonts', {
+      value: {
+        ready: fontsReadyPromise,
+      },
+      configurable: true,
+    })

     render(
       <FontLoaderWrapper>
         <div>Content</div>
       </FontLoaderWrapper>
     )

     expect(thenSpy).toHaveBeenCalled()
   })
frontend/__tests__/unit/components/SortBy.test.tsx-131-139 (1)

131-139: ⚠️ Potential issue | 🟡 Minor

Make sortOptions optional in the component's prop types or remove the sortOptions={undefined} test case.

The SortByProps type defines sortOptions as a required property ({ key: string; label: string }[]), not optional. The test passes sortOptions={undefined}, which would be a TypeScript error in strict mode. While the component defensively handles undefined (line 14: if (!sortOptions || sortOptions.length === 0) return null), either the type definition should be updated to sortOptions?: { key: string; label: string }[] to match the implementation's expectations, or the test should be removed since the prop is required by contract.

frontend/__tests__/unit/utils/formValidationUtils.test.ts-319-321 (1)

319-321: ⚠️ Potential issue | 🟡 Minor

Incorrect inline comment and inconsistent assertion matcher.

The comment states (undefined && true) = false, but in JavaScript undefined && true evaluates to undefined, making the full expression false || undefined = undefined (falsy, but not false). toBeFalsy() passes here but masks the actual value, and every other shouldValidate assertion in this suite uses the stricter toBe(false).

Consider using toBe(undefined) (to match the real runtime value) or better yet, check whether the production code should be normalized to always return a boolean — e.g. with Boolean(...) or !! — so all shouldValidate values are truly boolean.

Proposed fix
-      // endedAt: (touched.endedAt ?? false) || (touched.startedAt && !!formData.endedAt)
-      // = (undefined ?? false) || (undefined && true) = false || false = false
-      expect(rules[3].shouldValidate).toBeFalsy()
+      // endedAt: (touched.endedAt ?? false) || (touched.startedAt && !!formData.endedAt)
+      // = (undefined ?? false) || (undefined && !!formData.endedAt) = false || undefined = undefined
+      expect(rules[3].shouldValidate).toBeFalsy()
frontend/__tests__/unit/components/ContributorAvatar.test.tsx-326-341 (1)

326-341: ⚠️ Potential issue | 🟡 Minor

Test name contradicts its assertion.

The test is named "renders avatar without &s=60 suffix when treated as non-Algolia" but line 340 asserts the src includes &s=60. The comments on lines 327-329 even acknowledge that the false branch can't actually be reached this way. This is misleading — a future reader will assume this test covers the non-Algolia path when it doesn't.

Either rename the test to reflect what it actually verifies, or restructure to genuinely exercise the false branch (e.g., by mocking the internal isAlgoliaContributor check).

frontend/__tests__/unit/global-error.test.tsx-173-182 (1)

173-182: ⚠️ Potential issue | 🟡 Minor

Wrap Error.captureStackTrace restoration in try/finally to prevent global state leakage.

If the AppError constructor or any assertion throws, Error.captureStackTrace is never restored, which can break subsequent tests across the entire suite.

Proposed fix
   test('should handle when Error.captureStackTrace is not available', () => {
     const originalCaptureStackTrace = Error.captureStackTrace
     delete (Error as unknown as { captureStackTrace?: unknown }).captureStackTrace
 
-    const err = new AppError(500, 'Test')
-    expect(err.name).toBe('AppError')
-    expect(err.statusCode).toBe(500)
-    ;(Error as unknown as { captureStackTrace?: unknown }).captureStackTrace =
-      originalCaptureStackTrace
+    try {
+      const err = new AppError(500, 'Test')
+      expect(err.name).toBe('AppError')
+      expect(err.statusCode).toBe(500)
+    } finally {
+      ;(Error as unknown as { captureStackTrace?: unknown }).captureStackTrace =
+        originalCaptureStackTrace
+    }
   })
frontend/__tests__/unit/components/Leaders.test.tsx-58-72 (1)

58-72: ⚠️ Potential issue | 🟡 Minor

The field names in the test data are correct; however, use member: null instead of member: undefined.

The userWithoutMember object correctly uses memberName and description as expected by the Leaders component. However, the Leader type definition specifies member: { ... } | null, so the test should set member: null rather than member: undefined to match the type contract. While the test will function correctly at runtime due to optional chaining and truthiness checks, the type is technically incorrect.

frontend/__tests__/unit/components/MentorshipPullRequest.test.tsx-128-132 (1)

128-132: ⚠️ Potential issue | 🟡 Minor

Locale-dependent date assertion may cause flaky tests.

toLocaleDateString() without an explicit locale produces different output depending on the environment (e.g., 1/15/2024 in en-US vs 15/01/2024 in en-GB). This can cause CI failures if the runner locale differs from the developer's machine. Pass an explicit locale to match the component's behavior.

Suggested fix
     test('renders date in correct format', () => {
       render(<MentorshipPullRequest pr={mockPullRequestOpen} />)
-      const dateStr = new Date('2024-01-15').toLocaleDateString()
+      const dateStr = new Date('2024-01-15').toLocaleDateString('en-US')
       expect(screen.getByText(new RegExp(dateStr))).toBeInTheDocument()
     })

Use the same locale the component uses for formatting, or match against a locale-independent pattern.

frontend/__tests__/unit/components/MentorshipPullRequest.test.tsx-134-141 (1)

134-141: ⚠️ Potential issue | 🟡 Minor

Fix toHaveStyle assertions to use object format instead of camelCase strings.

The jest-dom toHaveStyle API expects CSS property names in kebab-case when using string format (e.g., 'background-color: #238636'), or as camelCase properties in an object (e.g., { backgroundColor: '#238636' }). Lines 137, 146, and 152 use camelCase in string format, which is incorrect.

Suggested fixes
-      expect(badge).toHaveStyle('backgroundColor: `#238636`')
+      expect(badge).toHaveStyle({ backgroundColor: '#238636' })
-      expect(badge).toHaveStyle('backgroundColor: `#8657E5`')
+      expect(badge).toHaveStyle({ backgroundColor: '#8657E5' })
-      expect(badge).toHaveStyle('backgroundColor: `#DA3633`')
+      expect(badge).toHaveStyle({ backgroundColor: '#DA3633' })
frontend/__tests__/unit/components/ModuleCard.test.tsx-726-730 (1)

726-730: ⚠️ Potential issue | 🟡 Minor

Regex doesn't properly validate negative duration output.

Math.ceil(-60/7) yields -8, so getSimpleDuration returns "-8 weeks". The regex /\d+ weeks?|0 weeks/ will still pass because toMatch finds "8 weeks" as a substring within "-8 weeks", masking the negative sign. If the intent is to document the actual negative-duration behavior, assert the exact expected output (e.g., "-8 weeks" or "0 weeks") so the test meaningfully captures the edge case.

frontend/__tests__/unit/components/TruncatedText.test.tsx-166-171 (1)

166-171: ⚠️ Potential issue | 🟡 Minor

Use JSX children syntax instead of the children prop.

Biome flags children={null} as an anti-pattern. Use the canonical JSX form instead.

Proposed fix
-    render(<TruncatedText text={undefined} children={null} />)
+    render(<TruncatedText text={undefined}>{null}</TruncatedText>)
frontend/__tests__/unit/components/ModuleForm.test.tsx-773-798 (1)

773-798: ⚠️ Potential issue | 🟡 Minor

Missing assertion — test comment says "should not include project-1" but never verifies it.

The test triggers a search with value: 'project-1' to exercise the filtering logic, but the final comment at Line 797 is just a description with no corresponding expect(). Add an assertion (e.g., check that the rendered autocomplete items don't include project-1).

frontend/__tests__/unit/components/CardDetailsPage.test.tsx-2220-2290 (1)

2220-2290: ⚠️ Potential issue | 🟡 Minor

EntityActions tests don't verify what they claim.

Both "renders EntityActions when user is admin" (Line 2221) and "does not render EntityActions when user is not admin" (Line 2258) only assert screen.getByText(/Test Project/), which is the page title and is always present. Neither test checks for the actual presence or absence of the entity-actions test ID.

Use screen.getByTestId('entity-actions') / screen.queryByTestId('entity-actions') to verify the component is or isn't rendered, respectively.

frontend/__tests__/unit/components/ProgramForm.test.tsx-806-838 (1)

806-838: ⚠️ Potential issue | 🟡 Minor

Assertions are trivially true — queryByText returns null, not undefined.

screen.queryByText('Start Date') returns null (not undefined) when not found, so expect(startDateLabel).not.toBeUndefined() always passes regardless. Use .toBeInTheDocument() or .not.toBeNull() to actually verify the element exists. The same applies to the end date test.

Proposed fix
-      expect(startDateLabel).not.toBeUndefined()
+      expect(startDateLabel).toBeInTheDocument()
frontend/__tests__/unit/components/CardDetailsPage.test.tsx-2369-2429 (1)

2369-2429: ⚠️ Potential issue | 🟡 Minor

Same issue: program EntityActions tests only assert page title, not EntityActions presence.

All three tests (Lines 2369, 2397, 2414) for program-level EntityActions rendering only check screen.getByText('Test Project'). They should verify entity-actions test ID presence/absence to match their stated intent.

frontend/__tests__/unit/components/ProgramForm.test.tsx-175-205 (1)

175-205: ⚠️ Potential issue | 🟡 Minor

Tests don't verify what their names claim.

Both "disables submit button when loading" (Line 175) and "enables submit button when not loading" (Line 191) only assert buttons.length >= 0, which is trivially always true. They should actually check the disabled attribute on the submit button to match their names.

Example fix for "disables submit button when loading"
       const buttons = screen.getAllByRole('button')
-      // Component should render without errors
-      expect(buttons.length).toBeGreaterThanOrEqual(0)
+      const submitButton = buttons.find((btn) => btn.textContent?.includes('Saving...'))
+      expect(submitButton).toBeDisabled()
frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx-421-434 (1)

421-434: ⚠️ Potential issue | 🟡 Minor

Test intent is unclear — full data is provided despite empty issueId.

The test name says "Loading issue… title when issueId is not available", but mockUseQuery still returns a fully populated mockIssueData. In production, an empty issueId would likely result in no data or a different query response. This makes the test assert on a state that probably can't occur in practice (full data + empty issueId simultaneously).

Consider either returning loading: true / data: undefined to match a realistic loading scenario, or clarifying the comment to explain that the component checks issueId independently of the query result.

frontend/__tests__/unit/components/ChapterMapWrapper.test.tsx-265-290 (1)

265-290: ⚠️ Potential issue | 🟡 Minor

Test doesn't verify _distance removal — only checks array length.

The test is named "correctly maps sorted data removing _distance property" but the mock ChapterMap only renders geoLocData.length. The assertion at line 288 (toHaveTextContent('2')) passes whether or not _distance is stripped. To actually verify the mapping, the mock would need to render or expose the properties of each chapter item (e.g., serialize and check that _distance is absent).

frontend/__tests__/unit/pages/Chapters.test.tsx-149-163 (1)

149-163: ⚠️ Potential issue | 🟡 Minor

Test doesn't actually verify the behavior it claims to test.

The test says "chapters data should be used for the map instead of geoLocData" when a search query is present (line 93 in page.tsx does have this conditional: geoLocData={searchQuery ? chapters : geoLocData}), but the only assertion—screen.getByText('Chapter 1')—is identical to what the existing 'renders chapter data correctly' test already verifies without any search query. Since both data sources are mocked to return the same mockChapterData.chapters, this assertion will pass regardless of which branch of the ternary is executed.

To meaningfully verify this behavior, assert something that differs between the two code paths—e.g., that ChapterMapWrapper receives chapters as its geoLocData prop when searchQuery is set, or that the map renders different content based on the data source.

🧹 Nitpick comments (46)
frontend/__tests__/unit/components/UserMenu.test.tsx (1)

777-812: Describe block covers only the leader path; the OWASP Staff link is untested.

The block is named "Project Leader and OWASP Staff links" but contains no test for the "Project Health Dashboard" link rendered when isOwaspStaff is true. Since the PR objective is to hit 80% coverage, this likely leaves the staff-link branch uncovered.

Consider adding a parallel test:

🧪 Suggested additional test
+    it('closes dropdown when Project Health Dashboard link is clicked', async () => {
+      const staffSession: ExtendedSession = {
+        user: {
+          name: 'Staff User',
+          email: 'staff@example.com',
+          image: 'https://example.com/avatar.jpg',
+          isLeader: false,
+          isOwaspStaff: true,
+        },
+        expires: '2024-12-31',
+      }
+
+      mockUseSession.mockReturnValue({
+        session: staffSession,
+        isSyncing: false,
+        status: 'authenticated',
+      })
+
+      render(<UserMenu isGitHubAuthEnabled={true} />)
+
+      const avatarButton = screen.getByRole('button')
+      fireEvent.click(avatarButton)
+
+      await waitFor(() => {
+        expect(screen.getByText('Project Health Dashboard')).toBeInTheDocument()
+      })
+
+      const dashboardLink = screen.getByText('Project Health Dashboard')
+      fireEvent.click(dashboardLink)
+
+      await waitFor(() => {
+        expect(avatarButton).toHaveAttribute('aria-expanded', 'false')
+      })
+    })
frontend/__tests__/unit/components/MetricsPDFButton.test.tsx (1)

48-59: Test 3 is duplicative of Test 2.

This test ("passes correct path and fileName props") does the exact same thing as the previous test ("calls fetchMetricsPDF when icon is clicked") — clicks the icon and asserts fetchMetricsPDF was called with the provided props. Only the prop values differ. Consider either removing this test or merging it into a test.each if you want to verify multiple prop combinations.

♻️ Consolidate into a parameterized test
-  it('calls fetchMetricsPDF when icon is clicked', async () => {
-    mockFetchMetricsPDF.mockResolvedValueOnce(undefined)
-
-    render(<MetricsPDFButton path="/api/metrics" fileName="metrics.pdf" />)
-
-    const icon = screen.getByTestId('tooltip').querySelector('svg')
-    expect(icon).toBeInTheDocument()
-
-    fireEvent.click(icon!)
-
-    await waitFor(() => {
-      expect(mockFetchMetricsPDF).toHaveBeenCalledWith('/api/metrics', 'metrics.pdf')
-    })
-  })
-
-  it('passes correct path and fileName props', async () => {
-    mockFetchMetricsPDF.mockResolvedValueOnce(undefined)
-
-    render(<MetricsPDFButton path="/custom/path" fileName="custom-file.pdf" />)
-
-    const icon = screen.getByTestId('tooltip').querySelector('svg')
-    fireEvent.click(icon!)
-
-    await waitFor(() => {
-      expect(mockFetchMetricsPDF).toHaveBeenCalledWith('/custom/path', 'custom-file.pdf')
-    })
-  })
+  it.each([
+    { path: '/api/metrics', fileName: 'metrics.pdf' },
+    { path: '/custom/path', fileName: 'custom-file.pdf' },
+  ])('calls fetchMetricsPDF with path=$path and fileName=$fileName on click', async ({ path, fileName }) => {
+    mockFetchMetricsPDF.mockResolvedValueOnce(undefined)
+
+    render(<MetricsPDFButton path={path} fileName={fileName} />)
+
+    const icon = screen.getByTestId('tooltip').querySelector('svg')
+    expect(icon).toBeInTheDocument()
+
+    fireEvent.click(icon!)
+
+    await waitFor(() => {
+      expect(mockFetchMetricsPDF).toHaveBeenCalledWith(path, fileName)
+    })
+  })
frontend/__tests__/unit/pages/Organization.test.tsx (1)

87-137: Mock data uses null where the Organization type expects undefined.

The Organization type in frontend/src/types/organization.ts defines company, email, and location as optional (string | undefined), but the mock data at lines 94, 97, 99, 110, 113, and 115 uses null. This works at runtime because the component likely uses falsy checks, but it bypasses TypeScript's type safety in tests. Consider using undefined instead of null to keep mocks aligned with the actual type, or omit those keys entirely.

Suggested diff
         {
           objectID: 'org-no-optional',
           avatarUrl: 'https://avatars.githubusercontent.com/u/999999?v=4',
           collaboratorsCount: 10,
-          company: null,
           createdAt: 1596744799,
           description: 'Organization without optional fields',
-          email: null,
           followersCount: 100,
-          location: null,
           login: 'no-optional-org',
           name: 'No Optional Fields Org',
           publicRepositoriesCount: 50,
           updatedAt: 1727390473,
           url: 'https://github.com/no-optional-org',
         },
frontend/__tests__/unit/pages/UserDetails.test.tsx (1)

895-917: Consider consolidating the two "name is null" tests.

Lines 895–917 and 942–963 both set up the same mock (user with name: null) but assert different things (login link presence vs. alt text). Merging them into a single test would reduce duplication of the mock setup without sacrificing clarity.

♻️ Suggested consolidation
-    test('renders with login as title when name is null', async () => {
-      const dataWithNullName = {
-        ...mockUserDetailsData,
-        user: {
-          ...mockUserDetailsData.user,
-          name: null,
-        },
-      }
-
-      ;(useQuery as unknown as jest.Mock).mockReturnValue({
-        data: dataWithNullName,
-        loading: false,
-        error: null,
-      })
-
-      render(<UserDetailsPage />)
-
-      await waitFor(() => {
-        // The page title should use login when name is null
-        const loginLink = screen.getByRole('link', { name: '@testuser' })
-        expect(loginLink).toBeInTheDocument()
-      })
-    })
-
-    test('renders placeholder avatar when avatarUrl is null', async () => {
...
-    })
-
-    test('renders with login as alt text when name is null', async () => {
-      const dataWithNullName = {
-        ...mockUserDetailsData,
-        user: {
-          ...mockUserDetailsData.user,
-          name: null,
-        },
-      }
-
-      ;(useQuery as unknown as jest.Mock).mockReturnValue({
-        data: dataWithNullName,
-        loading: false,
-        error: null,
-      })
-
-      render(<UserDetailsPage />)
-
-      await waitFor(() => {
-        const avatar = screen.getByAltText('testuser')
-        expect(avatar).toBeInTheDocument()
-      })
-    })
+    test('uses login as title and alt text when name is null', async () => {
+      const dataWithNullName = {
+        ...mockUserDetailsData,
+        user: {
+          ...mockUserDetailsData.user,
+          name: null,
+        },
+      }
+
+      ;(useQuery as unknown as jest.Mock).mockReturnValue({
+        data: dataWithNullName,
+        loading: false,
+        error: null,
+      })
+
+      render(<UserDetailsPage />)
+
+      await waitFor(() => {
+        const loginLink = screen.getByRole('link', { name: '@testuser' })
+        expect(loginLink).toBeInTheDocument()
+        const avatar = screen.getByAltText('testuser')
+        expect(avatar).toBeInTheDocument()
+      })
+    })

Also applies to: 942-963

frontend/__tests__/unit/components/ToggleableList.test.tsx (1)

247-275: Consider adding a Space-key test for the disabled state.

You test both click and Enter on a disabled button, but Space navigation (tested in the enabled case at line 219) has no corresponding disabled counterpart. For symmetry and completeness:

Suggested addition
+  it('does not navigate when pressing Space key on disabled item button', () => {
+    render(
+      <ToggleableList
+        entityKey="test"
+        items={['React', 'Next.js', 'FastAPI']}
+        label="Tags"
+        limit={2}
+        isDisabled={true}
+      />
+    )
+    const button = screen.getByText('React')
+    fireEvent.keyDown(button, { key: ' ' })
+    expect(mockPush).not.toHaveBeenCalled()
+  })
frontend/__tests__/unit/components/StructuredDataScript.test.tsx (1)

109-128: XSS test doesn't verify the sanitized output.

The test asserts that DOMPurify.sanitize was called and that the input contains "John", but it never checks that the rendered script tag's innerHTML is free of the <script>alert("xss")</script> payload. This means the test would pass even if the component ignored the sanitizer's return value.

Proposed fix: assert on rendered output
     render(<StructuredDataScript data={maliciousData} />)
 
     expect(mockDOMPurify).toHaveBeenCalled()
     const callArg = mockDOMPurify.mock.calls[0][0]
     expect(callArg).toContain('John')
+
+    const { container } = render(<StructuredDataScript data={maliciousData} />)
+    const script = container.querySelector('script')
+    expect(script?.innerHTML).not.toContain('<script>')
+    expect(script?.innerHTML).toContain('John')

Or more concisely, capture the container from the existing render call:

-    render(<StructuredDataScript data={maliciousData} />)
+    const { container } = render(<StructuredDataScript data={maliciousData} />)
 
     expect(mockDOMPurify).toHaveBeenCalled()
     const callArg = mockDOMPurify.mock.calls[0][0]
     expect(callArg).toContain('John')
+
+    const script = container.querySelector('script')
+    expect(script?.innerHTML).not.toContain('<script>')
frontend/__tests__/unit/components/ContributionHeatmap.test.tsx (3)

196-227: Weak assertions — these tests verify rendering but not the fallback behavior they claim to test.

The invalid-date tests (lines 196–227) only assert toBeInTheDocument(), which passes even if the component silently breaks its date logic. To actually validate fallback behavior, assert on data-series-length (like the missing-date tests above do) or check that the rendered data range matches the expected default 1-year range.


229-250: Near-duplicate tests for the same swapped-date scenario.

Both tests verify startDate > endDate handling. The second test (line 241) is strictly weaker — it uses different dates but only asserts toBeInTheDocument(). Consider consolidating into a single test or, if keeping both, strengthen the second to assert the swap actually occurred (e.g., verify the resulting series data reflects the corrected range).


7-53: Mock exercises tooltip.custom as a render side-effect purely for coverage — consider testing tooltip output explicitly.

The mock invokes tooltip.custom() during render (lines 22–34) but discards the return value. This achieves line coverage of the tooltip formatter but won't catch regressions in its output (e.g., wrong date format, wrong unit label). Consider capturing the return value and asserting on it in the tooltip-focused tests (lines 328–372), or at minimum storing it in a variable the tests can inspect.

frontend/__tests__/unit/pages/MyMentorship.test.tsx (1)

263-264: globalThis.scrollTo is overridden but never restored after the test.

This leaks the mock to subsequent tests. Use jest.spyOn or restore the original in an afterEach block.

Suggested fix using spyOn
-    const scrollToMock = jest.fn()
-    globalThis.scrollTo = scrollToMock
+    const scrollToMock = jest.fn()
+    jest.spyOn(globalThis, 'scrollTo').mockImplementation(scrollToMock)

This will be automatically restored by jest.clearAllMocks() in the existing beforeEach, or you can add jest.restoreAllMocks() if needed.

frontend/__tests__/unit/pages/ProjectHealthDashboardMetricsDetails.test.tsx (1)

132-156: Test data matches the default mock — consider noting this for clarity.

The overrides on lines 139-140 (isFundingRequirementsCompliant: true, isLeaderRequirementsCompliant: false) are identical to the values in mockProjectsDashboardMetricsDetailsData. The test still works and the compliance-text assertions add coverage, but the explicit "override" is a no-op. This is fine functionally — just noting it for clarity so future readers don't assume a different scenario is being set up.

frontend/__tests__/unit/pages/About.test.tsx (2)

606-697: Tests don't actually verify the progress-state rendering differences.

Each test name implies it validates a distinct visual state ("Not Started", "In Progress", "Completed"), but all three only assert that the milestone title text appears in the document. There is no assertion on the progress bar value, status label/icon, or any CSS class that distinguishes the three states. As written, these tests would still pass even if the component ignored the progress field entirely.

Consider asserting on the actual progress indicator (e.g., a status icon, a progress bar width/value, or a status label rendered by the component) to make the tests meaningful for their stated purpose. Alternatively, consolidate into a single parameterized test using test.each and add progress-specific assertions.


556-582: Duplicate of the "handles null project" test at lines 460–488.

This test (renders ErrorDisplay when project is null) asserts the same behavior and uses the same mock setup as handles null project in data response gracefully (lines 460–488). One of them can be removed.

frontend/__tests__/unit/pages/EditModule.test.tsx (4)

192-193: Misleading comment — this test is about a query error, not access denial.

The comment "When denied but formData is null" doesn't match the test scenario, which mocks a GraphQL query error. Consider updating it to reflect the actual intent, e.g., "When query errors out, data is null so the component shows the spinner."


214-215: Same misleading comment as above — this is about an unauthenticated user, not denial.

Update the comment to reflect that the unauthenticated session prevents form data from being prepared, hence the spinner.


305-308: Redundant assertion — both lines verify the same thing.

Line 305 already waits for and asserts the "Minimal Module" value is present. Line 307 re-checks the same field via getByLabelText. Consider dropping one or replacing Line 307 with an assertion on a different optional field (e.g., verifying description is empty, or domains field has a fallback value) to get more coverage value from this test.


176-194: Consider verifying that an error toast or redirect is (or isn't) triggered in these error/unauthenticated states.

Both tests only assert the loading spinner is shown. For stronger coverage, you could additionally verify that addToast was or wasn't called, and that no navigation occurred, to confirm the component doesn't have unintended side effects in these states.

Also applies to: 196-216

frontend/__tests__/unit/components/FontLoaderWrapper.test.tsx (1)

92-92: Nit: misleading test description.

"updates children when data prop changes" — FontLoaderWrapper has no data prop; this test rerenders with different children. Consider renaming to something like "updates rendered output when children change".

frontend/__tests__/unit/components/SingleModuleCard.test.tsx (1)

396-411: Assertion could be more precise.

Since mentors is set to [], only one show-more button (for mentees) should be present. Using toHaveLength(1) instead of toBeGreaterThan(0) would make the test more specific and catch unexpected extra buttons.

       const showMoreButtons = screen.getAllByTestId('show-more-button')
-      expect(showMoreButtons.length).toBeGreaterThan(0)
+      expect(showMoreButtons).toHaveLength(1)
frontend/__tests__/unit/utils/formValidationUtils.test.ts (2)

20-22: Redundant test — duplicates the empty-string case on Line 13.

This test is described as "null-like empty" but passes an empty string '' exactly like the first test; only fieldName differs ('Name' vs 'Field'). It doesn't exercise any new branch. Consider either removing it or replacing it with a genuinely distinct input (e.g., testing with a value coerced through the !value check such as null cast via as unknown as string, if that's a realistic call-site scenario).


148-165: Consider extracting a shared defaultFormData fixture to reduce duplication.

The same formData object is redeclared in nearly every test. A single const defaultFormData = { ... } at the describe scope (overridden per-test with spread where needed) would cut the boilerplate significantly.

Also applies to: 186-203, 205-222, 224-241, 243-260, 262-279, 281-298, 300-314, 324-342, 344-362, 364-382, 384-402, 404-424

frontend/__tests__/unit/components/ContributorAvatar.test.tsx (3)

246-261: Casting away a required field doesn't reliably test the intended branch.

Omitting avatarUrl via as unknown as Contributor means the component receives undefined for a required prop. This doesn't necessarily exercise the isAlgoliaContributor === false branch in a controlled way — it depends on how the component internally derives that flag. The test only asserts the element exists, without verifying any meaningful behavioral difference for the "non-Algolia" path.

If the goal is to cover the false branch of isAlgoliaContributor, consider mocking or spying on the logic that determines this flag, so the test is deterministic and documents the expected behavior difference.


263-279: Test name is misleading — duplicates an existing test.

This test is named "shows repository info in tooltip when projectName is present" but the assertion on line 278 confirms that projectName is not shown. The comment on line 277 explains this, but the test name suggests the opposite.

Additionally, this is functionally identical to the existing test on lines 210-217 ("does not include project name in tooltip"). Consider removing this duplicate or renaming it to avoid confusion.


244-359: Several tests in this block overlap with existing tests above.

The new describe('branch coverage for isAlgoliaContributor') block was intended to improve branch coverage, but multiple tests here duplicate scenarios already covered:

  • Lines 263-279 ≈ Lines 210-217 (projectName not shown in tooltip)
  • Lines 281-298 ≈ Lines 133-149 (no contributions shows only name)

Focus on tests that exercise genuinely uncovered branches rather than re-asserting existing behavior with slightly different fixture data.

frontend/__tests__/unit/pages/ProjectsHealthDashboardMetrics.test.tsx (2)

267-337: Extract repeated mock setup into a helper to reduce duplication.

Every filter, sort, and reset test repeats the identical 7-line mockFetchMore + useQuery setup. A small helper would eliminate ~150 lines of boilerplate.

♻️ Suggested helper
+  const renderWithFetchMore = () => {
+    const mockFetchMore = jest.fn()
+    ;(useQuery as unknown as jest.Mock).mockReturnValue({
+      data: mockHealthMetricsData,
+      loading: false,
+      error: null,
+      fetchMore: mockFetchMore,
+    })
+    render(<MetricsPage />)
+    return { mockFetchMore }
+  }

Then each test becomes:

   test('applies level filter - incubator', async () => {
-    const mockFetchMore = jest.fn()
-    ;(useQuery as unknown as jest.Mock).mockReturnValue({
-      data: mockHealthMetricsData,
-      loading: false,
-      error: null,
-      fetchMore: mockFetchMore,
-    })
-    render(<MetricsPage />)
+    renderWithFetchMore()
 
     const incubatorButton = screen.getByRole('button', { name: 'Incubator' })
     fireEvent.click(incubatorButton)
     ...

580-665: URL-param initialization tests only check that the page renders — they don't verify the params had any effect.

These tests set order, health, or level URL params, then assert the heading exists. The heading renders regardless of params, so these tests would pass even if the URL-param parsing code were deleted. Consider asserting that the query variables or initial component state reflect the URL params.

frontend/__tests__/unit/global-error.test.tsx (1)

19-36: Module-level shouldThrowError flag lacks centralized reset.

The mutable flag is toggled manually inside individual tests (set before render, reset at end). If an assertion throws before the reset line, the flag leaks into subsequent tests. Consider adding an afterEach in the ErrorWrapper describe block (or at the top level) to always reset it:

Suggested improvement
 describe('ErrorWrapper component', () => {
+  afterEach(() => {
+    shouldThrowError = false
+  })
+
   test('renders children without crashing', () => {
-    shouldThrowError = false
     render(
frontend/__tests__/unit/pages/EditProgram.test.tsx (3)

43-51: Nit: clearAllMocks after mock setup is confusing ordering.

jest.clearAllMocks() on line 50 only clears call/instance tracking (not mockReturnValue implementations), so this works correctly. However, placing it after the mock setup is counterintuitive. Conventionally, clearing comes first so readers don't have to reason about what clearAllMocks resets vs. doesn't.

Suggested reorder
  beforeEach(() => {
+   jest.clearAllMocks()
    ;(useRouter as jest.Mock).mockReturnValue({ push: mockPush, replace: mockReplace })
    ;(useParams as jest.Mock).mockReturnValue({ programKey: 'program_1' })
    ;(useApolloClient as jest.Mock).mockReturnValue({
      query: mockQuery,
    })
    ;(useMutation as unknown as jest.Mock).mockReturnValue([mockUpdateProgram, { loading: false }])
-   jest.clearAllMocks()
  })

78-80: Redundant waitFor wrapping findByText.

screen.findByText already polls/waits internally (it returns a promise that resolves when the element appears). Wrapping it in waitFor is unnecessary and adds noise. This pattern is repeated across multiple tests in this file.

For example, this:

await waitFor(async () => {
  expect(await screen.findByText('Access Denied')).toBeInTheDocument()
})

can be simplified to:

expect(await screen.findByText('Access Denied')).toBeInTheDocument()

Also applies to: 127-129, 144-146, 324-326, 346-348


276-298: GraphQL error test doesn't assert on the error itself.

This test sets error: new Error('GraphQL error') but only asserts the form renders. It doesn't verify any error-handling behavior (e.g., error message display, toast). If the component is expected to handle or surface this error, consider asserting that. If the component intentionally ignores query errors when data is still available, a brief comment explaining the intent would help.

frontend/__tests__/unit/pages/CreateModule.test.tsx (1)

120-127: High timeouts (10s waitFor, 15s test) indicate potential test fragility.

These timeouts are significantly above typical values and suggest the interaction chain (type → query → select → submit → navigate) may be slow or flaky in CI. This is acceptable as a pragmatic choice for a complex integration-style test, but consider whether the autocomplete/project-search interaction (lines 88-116) could be simplified by mocking at a higher level to reduce latency.

frontend/__tests__/unit/components/MentorshipPullRequest.test.tsx (2)

107-113: Weak assertion for avatar placeholder behavior.

expect(images.length).toBeLessThan(2) is overly permissive — it passes even if 0 images render (including when the component renders nothing at all). Consider directly asserting that the placeholder element exists, or at minimum assert images.length equals the exact expected count.

Suggested improvement
     test('renders placeholder when author has no avatar URL', () => {
       const { container } = render(<MentorshipPullRequest pr={mockPullRequestNoAuthor} />)
-      // When no avatar URL, a div placeholder should be rendered instead of Image
-      const images = container.querySelectorAll('img')
-      // Only the TruncatedText link should have an image, not the avatar
-      expect(images.length).toBeLessThan(2)
+      // When no avatar URL, a placeholder div is rendered instead of an img element
+      const placeholder = container.querySelector('div.rounded-full')
+      expect(placeholder).toBeInTheDocument()
     })

Adjust the selector to match whatever placeholder element the component actually renders.


155-160: Overlaps with the link test on lines 120–126.

This test checks target="_blank" and rel="noopener noreferrer" on the first link, which is already covered by the earlier test. Consider either removing this test or differentiating it (e.g., verifying that all links in the component have safe attributes, not just the first one).

frontend/__tests__/unit/components/forms/shared/FormContainer.test.tsx (1)

112-124: Test name is misleading — FormContainer doesn't prevent default submission.

The component simply passes onSubmit through to the <form> element without calling preventDefault() itself. This test only proves that the mock handler you wrote calls preventDefault(), not that the component does. Consider renaming this test (e.g., "calls onSubmit handler on form submit") or removing it since Lines 31–37 already cover that scenario.

frontend/__tests__/unit/components/forms/shared/FormButtons.test.tsx (1)

121-129: Duplicate test — already covered at Lines 110–119.

The test at Line 121 (does not call router.back when onCancel is provided) is fully redundant with the test at Line 110 which already asserts expect(mockBack).not.toHaveBeenCalled() on Line 118. Consider removing this duplicate.

frontend/__tests__/unit/components/TruncatedText.test.tsx (1)

203-216: No-op assertion expect(true).toBe(true) — consider using a more meaningful assertion.

While this is a known workaround for the jest/expect-expect ESLint rule, the test's intent (no error thrown after unmount + resize) would be better expressed with expect(() => window.dispatchEvent(new Event('resize'))).not.toThrow() inside act, which is both self-documenting and satisfies the linter. Based on learnings, expect(true).toBe(true) no-op assertions are intentionally added as workarounds, but a more descriptive alternative exists here.

frontend/__tests__/unit/components/ModuleForm.test.tsx (1)

589-658: Several selection-handling tests lack meaningful assertions after the action under test.

  • Line 616–618: Comment acknowledges the assertion gap ("items array might not have project-1") but no follow-up.
  • Line 628–631: Only asserts the button still exists after "all" selection — doesn't verify onProjectChange was (or wasn't) called.
  • Line 654–658: No assertion at all after clicking "Select Single Key."

These tests exercise code paths but don't verify observable outcomes, reducing their value as regression guards. Consider asserting on mockOnProjectChange calls or input value changes.

frontend/__tests__/unit/pages/Header.test.tsx (5)

502-518: Outside-click test doesn't assert the actual behavior it claims to test.

This test is titled "handles outside click correctly" but only asserts that addEventListener was called with 'click' — it never verifies that the menu actually closes after the outside click on document.body. The dedicated "Outside Click Handler" tests (lines 827–979) already do this properly.

Consider either adding a menu-state assertion or removing this test in favor of the more thorough suite below.


741-784: Interaction between beforeEach mock and per-test spy is fragile.

beforeEach (line 189) assigns window.addEventListener = jest.fn(), then this test spies on it again with jest.spyOn(globalThis, 'addEventListener'). The spy wraps the already-mocked jest.fn(), so it records calls but the underlying handler is never truly registered on the window. The test works because it extracts and invokes the handler manually, but the layered mocking is confusing and easy to break.

A cleaner approach: remove the blanket window.addEventListener = jest.fn() from beforeEach and use jest.spyOn consistently across all tests that need to inspect event registration. Tests that only need to assert registration can use the spy's mock.calls.


828-868: Potential stale-handler issue: mockImplementation prevents real registration.

The custom mockImplementation at line 834 pushes click handlers into a local array but never calls the real addEventListener, so the component's handler is never truly attached to window. This is fine because the test invokes the handler directly (line 861), but it means the test is coupled to the internal registration order. If the component ever re-orders its useEffect hooks or batches handler registrations differently, grabbing clickHandlers[clickHandlers.length - 1] may silently pick the wrong handler.

This isn't a blocking issue — just flagging it as a maintenance risk.


1080-1095: Fragile mobile-link identification via CSS class name.

return link.className.includes('transition')

Identifying the mobile menu's "About" link by checking for a transition CSS class is brittle — any Tailwind/styling refactor could break this selector without any functional change to the component. Consider querying within the mobile menu container (e.g., findMobileMenu()?.querySelectorAll('a')) or using a more semantic selector to isolate the mobile instance.


610-614: "Handles missing headerLinks gracefully" doesn't actually test missing data.

The mock in jest.mock('utils/constants', ...) always provides headerLinks, so this test is identical to the "handles undefined pathname gracefully" test above it — both just confirm the component renders without throwing under normal conditions. To truly test missing headerLinks, the mock would need to be overridden to return an empty or undefined array for this specific test.

frontend/__tests__/unit/components/DashboardWrapper.test.tsx (1)

36-38: Consider using resetAllMocks instead of clearAllMocks.

jest.clearAllMocks() only clears call history (mock.calls, etc.) but preserves mockReturnValue and mockImplementation from prior tests. This creates implicit coupling between test cases (as seen in the "handles status transitions" test). Using jest.resetAllMocks() would additionally clear return values and implementations, ensuring each test starts with a clean slate.

Proposed fix
   beforeEach(() => {
-    jest.clearAllMocks()
+    jest.resetAllMocks()
+    // Re-apply the notFound mock since resetAllMocks clears implementations
+    ;(notFound as jest.Mock).mockImplementation(() => {
+      throw new Error('notFound')
+    })
   })
frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx (1)

502-528: Brittle DOM queries using CSS class selectors.

Both placeholder avatar tests use document.querySelector('[aria-hidden="true"].rounded-full.bg-gray-400') which couples the test to Tailwind class names. If the styling changes (e.g., bg-gray-400bg-gray-300), these tests break without any functional regression.

Prefer adding a data-testid="placeholder-avatar" to the component, or at minimum query by role or other semantic attributes.

Also applies to: 546-570

frontend/__tests__/unit/components/ChapterMapWrapper.test.tsx (2)

27-57: mockOnShareLocation is captured but never used in assertions.

mockOnShareLocation (line 27) has its implementation set to props.onShareLocation each time the mock renders (line 38), but no test ever calls or asserts on mockOnShareLocation. The share-location button's onClick directly invokes props.onShareLocation (line 50), so the real handler is exercised without mockOnShareLocation. This variable is dead code and can be removed to reduce confusion.

♻️ Suggested cleanup
-const mockOnShareLocation = jest.fn()
 jest.mock('components/ChapterMap', () => {
   return function MockChapterMap(props: {
     geoLocData: Chapter[]
     showLocal: boolean
     style: React.CSSProperties
     userLocation?: { latitude: number; longitude: number } | null
     onShareLocation?: () => void
   }): JSX.Element {
-    // Capture the onShareLocation prop for testing
-    if (props.onShareLocation) {
-      mockOnShareLocation.mockImplementation(props.onShareLocation)
-    }
     return (
       <div data-testid="chapter-map">

243-263: Redundant console.error spy — already mocked in beforeEach.

Line 87 already spies on console.error with a no-op implementation for every test. The additional jest.spyOn(console, 'error') at line 246 creates a second spy wrapping the first, which works but is unnecessary. You can simply reference console.error directly in the assertion.

♻️ Suggested simplification
   it('logs error when geolocation throws an error', async () => {
     const mockError = new Error('Geolocation permission denied')
     ;(geolocationUtils.getUserLocationFromBrowser as jest.Mock).mockRejectedValue(mockError)
-    const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {})

     const ChapterMapWrapper = await getChapterMapWrapper()
     const { getByTestId, queryByTestId } = render(
       <ChapterMapWrapper {...defaultProps} showLocationSharing={true} />
     )

     await waitFor(() => {
       expect(getByTestId('share-location-btn')).toBeInTheDocument()
     })

     fireEvent.click(getByTestId('share-location-btn'))

     await waitFor(() => {
-      expect(consoleSpy).toHaveBeenCalledWith('Error detecting location:', mockError)
+      expect(console.error).toHaveBeenCalledWith('Error detecting location:', mockError)
       expect(queryByTestId('user-location')).not.toBeInTheDocument()
     })
   })

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: 5

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/pages/ProjectsHealthDashboardMetrics.test.tsx (1)

70-72: ⚠️ Potential issue | 🟡 Minor

clearAllMocks doesn't reset mockReturnValue — tests that override useSearchParams will leak.

jest.clearAllMocks() only clears call history; it does not restore the implementation set by mockReturnValue. The URL-param tests (lines 588–689) override useSearchParams via mockReturnValue, and that override persists into subsequent tests. Today this works by accident because those tests happen to be last, but reordering or adding tests will cause surprising failures.

Switch to restoreAllMocks or explicitly reset useSearchParams in afterEach:

Option A — restore in afterEach
   afterEach(() => {
-    jest.clearAllMocks()
+    jest.restoreAllMocks()
   })

Note: restoreAllMocks requires mocks created with jest.spyOn. Since these are jest.fn() inside jest.mock factories, the safer alternative is:

   afterEach(() => {
     jest.clearAllMocks()
+    const { useSearchParams } = jest.requireMock('next/navigation')
+    ;(useSearchParams as jest.Mock).mockImplementation(() => new URLSearchParams())
   })
🤖 Fix all issues with AI agents
In `@frontend/__tests__/unit/components/CardDetailsPage.test.tsx`:
- Around line 2325-2337: The test "does not render mentees section when no
mentees are provided" uses screen.getAllByTestId('contributors-list') which
throws when no matches exist; change that call to
screen.queryAllByTestId('contributors-list') in the CardDetailsPage.test.tsx
test so the negative assertion can safely check for absence (keep the rest of
the test logic that finds a list containing 'Mentees' and asserts it is
undefined or not present).
- Around line 2224-2259: The test titled "renders EntityActions for module type
when user is an admin" is asserting the page title instead of verifying
EntityActions; update the assertions in CardDetailsPage.test.tsx to explicitly
check the mocked EntityActions render by using
screen.getByTestId('entity-actions') for positive cases and
screen.queryByTestId('entity-actions') for negative cases (the "does not render"
tests), ensuring you target the CardDetailsPage render and the mocked
EntityActions component instead of relying on other text like "Test Project".

In `@frontend/__tests__/unit/components/ProgramForm.test.tsx`:
- Around line 293-301: The test currently uses buttons.find(...) to get
submitButton and guards with if (submitButton) which can silently skip the click
and let the test pass vacuously; replace that pattern by asserting the button
exists and then performing the click — e.g., use a direct query
(screen.getByRole or screen.getByText) to retrieve the submit button or add an
explicit expect(submitButton).toBeInTheDocument() before calling await
user.click(submitButton), updating every occurrence (the submitButton lookup and
click logic in these tests) so mockOnSubmit is actually exercised.
- Around line 226-240: The test named "calls setFormData when mentees limit
changes" is vacuous; update it to actually find the mentees limit input in the
ProgramForm (e.g., locate by label text/placeholder or role used by the
component), simulate changing its value (use fireEvent.change or userEvent.type
on that input), and assert that the mockSetFormData prop was called with the
expected updated formData (or at least called once with a payload containing the
new mentees limit). If the component updates asynchronously, wrap the assertion
in waitFor; reference ProgramForm, setFormData, and mockSetFormData to locate
the relevant props and behavior.
- Around line 804-836: The two tests named "handles start date changes and
triggers touched state" and "handles end date changes" in ProgramForm.test.tsx
only assert label presence; either rename them to reflect that they only check
rendering or update them to perform the interactions: render <ProgramForm ...>,
locate the actual date inputs via screen.getByLabelText('Start Date') /
screen.getByLabelText('End Date'), use fireEvent.change (or userEvent) to change
the value, and assert mockSetFormData (or the touched state handler) is called
with the expected updated formData; keep references to ProgramForm,
mockSetFormData, and the test titles when making the change.
🧹 Nitpick comments (14)
frontend/__tests__/unit/components/DashboardWrapper.test.tsx (1)

146-156: Redundant rerender before any state change.

The rerender at lines 152–156 uses the same mock value as the initial render at line 146 — no state has changed, so it exercises nothing new. You can remove it without losing coverage.

Proposed fix
     const { rerender } = render(
       <DashboardWrapper>
         <div>Dashboard Content</div>
       </DashboardWrapper>
     )
 
-    rerender(
-      <DashboardWrapper>
-        <div>Dashboard Content</div>
-      </DashboardWrapper>
-    )
-
     expect(screen.getByTestId('loading-spinner')).toBeInTheDocument()
frontend/__tests__/unit/pages/About.test.tsx (2)

606-706: Consider using test.each to reduce duplication across the three milestone tests.

All three tests are identical except for progress and title. A parameterized test would cut ~70 lines down to ~30 and make it trivial to add more boundary values later.

♻️ Suggested refactor
-  test('renders milestone with progress 0 - Not Started', async () => {
-    ;(useQuery as unknown as jest.Mock).mockImplementation((query) => {
-      if (query === GetAboutPageDataDocument) {
-        return {
-          loading: false,
-          data: {
-            project: {
-              ...mockAboutData.project,
-              recentMilestones: [
-                {
-                  ...mockAboutData.project.recentMilestones[0],
-                  progress: 0,
-                  title: 'Not Started',
-                },
-              ],
-            },
-            topContributors: mockAboutData.topContributors,
-            leader1: mockAboutData.users['arkid15r'],
-            leader2: mockAboutData.users['kasya'],
-            leader3: mockAboutData.users['mamicidal'],
-          },
-          error: null,
-        }
-      }
-      return { loading: true }
-    })
-    await act(async () => {
-      render(<About />)
-    })
-    await waitFor(() => {
-      expect(screen.getByText('Not Started')).toBeInTheDocument()
-    })
-  })
-
-  test('renders milestone with progress 50 - In Progress', async () => {
-    // ... ~30 nearly identical lines ...
-  })
-
-  test('renders milestone with progress 100 - Completed', async () => {
-    // ... ~30 nearly identical lines ...
-  })
+  test.each([
+    { progress: 0, title: 'Not Started' },
+    { progress: 50, title: 'In Progress Milestone' },
+    { progress: 100, title: 'Completed' },
+  ])('renders milestone with progress $progress - $title', async ({ progress, title }) => {
+    ;(useQuery as unknown as jest.Mock).mockImplementation((query) => {
+      if (query === GetAboutPageDataDocument) {
+        return {
+          loading: false,
+          data: {
+            project: {
+              ...mockAboutData.project,
+              recentMilestones: [
+                {
+                  ...mockAboutData.project.recentMilestones[0],
+                  progress,
+                  title,
+                },
+              ],
+            },
+            topContributors: mockAboutData.topContributors,
+            leader1: mockAboutData.users['arkid15r'],
+            leader2: mockAboutData.users['kasya'],
+            leader3: mockAboutData.users['mamicidal'],
+          },
+          error: null,
+        }
+      }
+      return { loading: true }
+    })
+    await act(async () => {
+      render(<About />)
+    })
+    await waitFor(() => {
+      expect(screen.getByText(title)).toBeInTheDocument()
+    })
+  })

635-637: Tests only assert title text, not progress-specific rendering.

Each test verifies the milestone title appears but doesn't check anything that varies with progress (e.g., progress bar width, status icon, CSS class). As-is, these tests would still pass if the component ignored the progress value entirely. Consider adding at least one assertion per test that validates the visual or structural difference driven by progress to make the coverage more meaningful.

Also applies to: 669-671, 703-705

frontend/__tests__/unit/components/ModuleCard.test.tsx (3)

284-310: preventDefault: jest.fn() in fireEvent.keyDown has no effect.

fireEvent.keyDown creates a real KeyboardEvent; properties like preventDefault in the event-init object are ignored (they aren't part of KeyboardEventInit). If the component calls e.preventDefault() on Enter/Space, these tests don't actually verify that. The toggle itself still fires because keyDown is dispatched, so the test passes — but the preventDefault assertion is absent.

If verifying preventDefault is a goal, use jest.spyOn on the event or check with userEvent. Otherwise, simply drop the preventDefault field to avoid implying it's tested.

Proposed cleanup
-      fireEvent.keyDown(showMoreButton, { key: 'Enter', preventDefault: jest.fn() })
+      fireEvent.keyDown(showMoreButton, { key: 'Enter' })
-      fireEvent.keyDown(showMoreButton, { key: ' ', preventDefault: jest.fn() })
+      fireEvent.keyDown(showMoreButton, { key: ' ' })

327-337: Test title overpromises — React keys aren't observable from the DOM.

The test name says it verifies key vs id priority, but it only asserts both modules render. React keys are internal and not reflected in the DOM. This is effectively a smoke test for the fallback code path, which is fine, but the title is misleading.

Consider renaming to something like "renders modules regardless of whether key or id is used" to match what's actually asserted.


726-730: Negative-duration test documents surprising behavior — consider flagging upstream.

getSimpleDuration('2024-03-01', '2024-01-01') returning '-8 weeks' is technically "working as coded," but it's almost certainly not a meaningful value for users. If the component is expected to guard against end < start, this test should assert a fallback (e.g., 'N/A' or 'Invalid duration'). If this is intentional, a brief comment explaining why would help future readers.

Not blocking, but worth raising with the component owner.

frontend/__tests__/unit/pages/ProjectsHealthDashboardMetrics.test.tsx (4)

215-267: Extract repeated mock setup into a helper to reduce ~100 lines of duplication.

Every filter and sort test (lines 215–568) repeats the same mockFetchMore + useQuery mock block verbatim. Additionally, mockFetchMore is never exercised in these tests — it's dead code here.

Suggested helper
+  const renderWithDefaults = () => {
+    ;(useQuery as unknown as jest.Mock).mockReturnValue({
+      data: mockHealthMetricsData,
+      loading: false,
+      error: null,
+      fetchMore: jest.fn(),
+    })
+    render(<MetricsPage />)
+  }

Then each filter/sort test becomes:

   test('applies health filter - healthy', async () => {
-    const mockFetchMore = jest.fn()
-    ;(useQuery as unknown as jest.Mock).mockReturnValue({
-      data: mockHealthMetricsData,
-      loading: false,
-      error: null,
-      fetchMore: mockFetchMore,
-    })
-    render(<MetricsPage />)
+    renderWithDefaults()
 
     const healthyButton = screen.getByRole('button', { name: 'Healthy' })
     fireEvent.click(healthyButton)

587-601: URL-param initialization tests only verify the page doesn't crash — not that params are actually consumed.

These tests (lines 587–689) set URL params like order=scoreAsc or health=healthy but only assert the heading renders. They won't catch a regression where the component silently ignores URL params.

Consider asserting that useQuery was called with variables reflecting the URL params, or that mockReplace was called to round-trip the params:

     render(<MetricsPage />)
-    await expectHeaderVisible()
-    // Verify component rendered without errors
-    expect(screen.getByRole('heading', { name: 'Project Health Metrics' })).toBeInTheDocument()
+    await waitFor(() => {
+      expect(useQuery).toHaveBeenCalledWith(
+        expect.anything(),
+        expect.objectContaining({
+          variables: expect.objectContaining({ orderBy: 'score_asc' }),
+        })
+      )
+    })

(Adjust variable names to match the actual GraphQL query shape.)


192-213: Pagination test could assert the page/offset variable passed to fetchMore.

The test confirms fetchMore was called and exercises updateQuery, but doesn't verify that the correct page or offset was requested. Consider:

     await waitFor(() => {
-      expect(mockFetchMore).toHaveBeenCalled()
+      expect(mockFetchMore).toHaveBeenCalledWith(
+        expect.objectContaining({
+          variables: expect.objectContaining({ page: 2 }),
+        })
+      )
     })

(Adjust the variable name to match your GraphQL query.)


122-161: The expect(true).toBe(true) lines are recognized as ESLint workarounds — no action needed.

These no-ops exist because jest/expect-expect doesn't detect assertions inside helper functions. Consider configuring assertFunctionNames in your ESLint config to recognize helpers like expectLoadingSpinnerExists and expectHeaderVisible so these placeholders can be removed. Based on learnings, this pattern can be resolved by configuring the ESLint rule's assertFunctionNames option.

frontend/__tests__/unit/pages/CreateModule.test.tsx (2)

120-127: Consider reducing the excessive timeouts.

A waitFor timeout of 10 seconds and a Jest timeout of 15 seconds are unusually high for a unit test with fully mocked dependencies. If the mocks resolve synchronously (as they should), these should complete in well under 1 second. Excessive timeouts mask flaky tests and slow down CI feedback when a test does fail (it hangs for up to 15 s before reporting).

If the project autocomplete involves a debounce (likely from the user.type(projectInput, 'Aw') on line 92), consider using jest.useFakeTimers() to advance past it deterministically, then reduce these timeouts to ~3–5 s as a safety margin.


199-229: Mixing jest.useFakeTimers() with waitFor is fragile.

waitFor uses real timers internally for its retry/polling loop. When fake timers are active, those internal timers are frozen, which can cause waitFor to hang or timeout if the assertion doesn't pass on the very first check. This is a well-known pitfall in Testing Library.

Two suggestions:

  1. Use advanceTimers option: Pass jest.useFakeTimers({ advanceTimers: true }) (Jest ≥ 27) so that Testing Library's internal timers still advance automatically.
  2. Move jest.useRealTimers() into afterEach: If the test fails before line 228, fake timers leak into subsequent tests.
♻️ Suggested improvement
  it('shows access denied and redirects when user is not an admin', async () => {
-   jest.useFakeTimers()
+   jest.useFakeTimers({ advanceTimers: true })
    ;(useSession as jest.Mock).mockReturnValue({

And for cleanup robustness, consider restoring timers in afterEach:

  afterEach(() => {
    jest.clearAllMocks()
+   jest.useRealTimers()
  })

Then remove the inline jest.useRealTimers() call at line 228.

frontend/__tests__/unit/components/ProgramForm.test.tsx (2)

940-960: Duplicate of the "marks fields as touched when submitting" test (lines 326–347).

This test has the exact same setup (render with defaultFormData), action (click Save), and assertion (mockOnSubmit not called) as the earlier test. Neither test actually verifies the touched state — both only verify that submission is blocked. Remove this duplicate or make it meaningfully different (e.g., assert that validation error messages appear after the click).


875-910: Async uniqueness check asserted synchronously — inconsistent with similar tests.

The name uniqueness check goes through an async Apollo query. Here the assertion at line 909 is synchronous, while the equivalent tests at lines 456–461 and 1074–1079 use waitFor with a timeout. With immediately-resolving mocks this likely works, but for consistency and resilience against timing changes, prefer wrapping in waitFor:

-      // Should prevent submission because 'Test Program' === 'test program' (case-insensitive)
-      expect(mockOnSubmit).not.toHaveBeenCalled()
+      await waitFor(() => {
+        expect(mockOnSubmit).not.toHaveBeenCalled()
+      })

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 (2)
frontend/__tests__/unit/components/ToggleableList.test.tsx (2)

205-317: ⚠️ Potential issue | 🔴 Critical

Critical: File has multiple syntax errors from overlapping test blocks that will fail to parse.

Biome confirms a parse error at Line 281, but the issue is systemic. New test blocks are opened but never closed before existing test blocks begin, creating illegally nested it() calls in at least four places:

New (unclosed) block opens at Existing it() illegally nested at
Line 205 Line 214
Line 249 Line 258
Line 281 Line 291
Line 298 Line 312

In each case the new it(...) render block is missing its closing }) and assertions before the next it( starts. This looks like a bad merge or copy-paste where new tests were inserted without removing or properly integrating with the existing overlapping tests.

You need to either:

  1. Remove the duplicates — keep one version of each test (the new or the existing), or
  2. Close each new block before the next it() and ensure both versions test meaningfully different things.

For example, at lines 205–219 you have two tests for "Enter key navigation" — one with limit={2} and three items, one with three different items and no limit. Decide which to keep (or keep both with proper closure):

Example fix for lines 205–219 (apply similar pattern to the other three locations)
   it('navigates when Enter key is pressed on item button', () => {
     render(
       <ToggleableList
         entityKey="test"
         items={['React', 'Next.js', 'FastAPI']}
         label="Tags"
         limit={2}
       />
     )
+    const button = screen.getByText('React')
+    fireEvent.keyDown(button, { key: 'Enter' })
+    expect(mockPush).toHaveBeenCalledWith('/projects?q=React')
+  })
+
   it('handles Enter key press on item button', () => {
     render(<ToggleableList entityKey="test" items={['React', 'Vue', 'Angular']} label="Tags" />)
     const button = screen.getByText('React')
     fireEvent.keyDown(button, { key: 'Enter' })
     expect(mockPush).toHaveBeenCalledWith('/projects?q=React')
   })

258-270: ⚠️ Potential issue | 🟡 Minor

Test at Line 265 ('prevents default behavior on keyboard event') doesn't actually verify preventDefault.

Even after fixing the syntax errors above, this test only asserts mockPush was called — it doesn't verify that preventDefault() was invoked. Either rename it to reflect what it actually tests, or add a proper preventDefault assertion:

  it('prevents default behavior on keyboard event', () => {
    render(<ToggleableList entityKey="test" items={['React']} label="Tags" />)
    const button = screen.getByText('React')
-   fireEvent.keyDown(button, { key: 'Enter' })
-   expect(mockPush).toHaveBeenCalled()
+   const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
+   const preventSpy = jest.spyOn(event, 'preventDefault')
+   button.dispatchEvent(event)
+   expect(preventSpy).toHaveBeenCalled()
  })
🤖 Fix all issues with AI agents
In `@frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx`:
- Around line 406-419: The test 'renders "(today)" for deadline that is exactly
today' is timezone-flaky because todayDeadline is built at UTC midnight; update
the test to produce a deadline at local midnight or else mock the current time:
change the construction of todayDeadline in the test (the variable todayDeadline
used in the dataWithTodayDeadline passed to mockUseQuery and rendered by
ModuleIssueDetailsPage) to use local-date midnight (e.g., using new Date(year,
month, date).toISOString()) or stub Date.now()/Date globally for the test so
ModuleIssueDetailsPage sees the intended "today" regardless of tester timezone.
🧹 Nitpick comments (3)
frontend/__tests__/unit/components/ToggleableList.test.tsx (1)

312-341: Redundant tests for disabled state — consider consolidating.

After fixing the syntax errors, you'll have many near-duplicate disabled-state tests: Lines 272–279, 291–296, 312–317, 319–323, 325–329, 331–335, 337–341 all test small variations of isDisabled. Consider consolidating into fewer, focused tests (e.g., one test for disabled interaction blocking across click/Enter/Space, one for CSS classes and the disabled attribute). This reduces maintenance burden without sacrificing coverage.

frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx (2)

502-528: Querying by Tailwind class names is brittle.

document.querySelector('[aria-hidden="true"].rounded-full.bg-gray-400') couples the test to specific Tailwind utility classes. Any styling refactor (e.g., changing the placeholder color) will break these tests without any behavioral change. Prefer adding a data-testid (e.g., data-testid="placeholder-avatar") to the placeholder element in the component and querying by that instead.

Also applies to: 546-570


421-434: Test name is slightly misleading.

The test is named 'shows "Loading issue…" title when issueId is not available' but it doesn't check for a page-level "Loading issue…" heading — it checks a button's title attribute. Consider renaming to something like 'assign button shows "Loading issue…" tooltip when issueId is empty' to make the intent clearer to future readers.

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: 4

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/pages/Header.test.tsx (1)

188-193: ⚠️ Potential issue | 🟠 Major

addEventListener/removeEventListener replaced by assignment — restoreAllMocks won't restore them.

Direct assignment (window.addEventListener = jest.fn()) bypasses Jest's spy tracking, so jest.restoreAllMocks() in afterEach never restores the originals. This leaks into subsequent test files. Use jest.spyOn instead so the originals are properly restored.

🐛 Proposed fix
-    // Mock window methods
-    window.addEventListener = jest.fn()
-    window.removeEventListener = jest.fn()
+    jest.spyOn(window, 'addEventListener')
+    jest.spyOn(window, 'removeEventListener')
🤖 Fix all issues with AI agents
In `@frontend/__tests__/unit/components/CardDetailsPage.test.tsx`:
- Around line 2368-2428: The tests for program EntityActions in
CardDetailsPage.test.tsx currently only assert the page title and never check
whether EntityActions is present; update the three tests ("renders program
EntityActions when type is program with appropriate access", "does not render
program EntityActions when canUpdateStatus is false", "does not render program
EntityActions when accessLevel is not admin") to assert on the EntityActions
element instead of just the title: when EntityActions should be present use
screen.getByTestId('entity-actions') or screen.getByText for a known action to
assert presence, and when it should not be present use
expect(screen.queryByTestId('entity-actions')).toBeNull() or
expect(...).not.toBeInTheDocument(); locate assertions in the tests that call
render(<CardDetailsPage {...programProps} />) and replace the existing
expect(screen.getByText('Test Project')) checks with the appropriate
presence/absence assertions for EntityActions.

In `@frontend/__tests__/unit/components/ModuleForm.test.tsx`:
- Around line 602-635: The test's comment is misleading: handleSelectionChange
in ModuleForm.tsx supports Set<React.Key> and triggers onProjectChange for the
selected project; update the test that currently only asserts
mockOnProjectChange was called with (null, 'Test') to also assert that
mockOnProjectChange was called with the selected project's id and name (e.g.,
'project-1', 'Test Project 1') after the Set selection via the select button,
and remove or revise the comment that claims the Set is ignored; reference the
test helpers renderProjectSelector, the mock mockOnProjectChange, and the
component method handleSelectionChange when locating where to add the additional
assertion.
- Around line 440-491: Update the two weak tests: in the "prevents default form
submission" test (using renderModuleForm and mockOnSubmit) actually fire a
submit event with a jest.fn() preventDefault handler (e.g.,
fireEvent.submit(form, { preventDefault })) and assert that preventDefault was
called and mockOnSubmit was not called; in the "sets all fields as touched on
submit" test, after rendering via renderModuleForm and submitting the form,
assert that each form field (name, description, startedAt, endedAt, projectId,
experienceLevel) is marked as touched by checking for validation feedback (e.g.,
presence of validation error messages) or field state (aria-invalid or a touched
CSS class) so the test verifies the touched behavior rather than only submitting
the form.

In `@frontend/__tests__/unit/components/TruncatedText.test.tsx`:
- Around line 203-216: The test in TruncatedText.test.tsx falsely asserts the
null-branch of checkTruncation because unmount() removes the resize listener
before the dispatched event; either remove the test or change it to actually
invoke the stored resize handler after unmount: spy on window.addEventListener
in the test to capture the handler passed for 'resize', call unmount(), then
call that captured handler (so checkTruncation runs with no element) and assert
no throw; reference the checkTruncation behavior and the resize event handler
captured via window.addEventListener in your change.
🧹 Nitpick comments (7)
frontend/__tests__/unit/components/ChapterMapWrapper.test.tsx (4)

7-26: Mock for next/dynamic works but the unused-param workaround is noisy.

The if (options) { /* intentionally unused */ } block (lines 13-15) exists only to silence a lint warning. A cleaner approach is to prefix the parameter with an underscore.

✏️ Suggested simplification
  return function mockDynamic(
    importFn: () => Promise<{ default: React.ComponentType<unknown> }>,
-   options?: { ssr?: boolean }
+   _options?: { ssr?: boolean }
  ) {
-   // Ignore options for SSR: false — reference it without using `void`
-   if (options) {
-     /* intentionally unused */
-   }
-   // Return a component that resolves the import synchronously for testing
    const Component = React.lazy(importFn)

245-265: Redundant console.error spy — beforeEach already installs one.

Line 89 already spies on console.error with a no-op implementation. The spy at line 248 creates a second layer on top. It works, but the local consoleSpy variable is unnecessary — you can assert directly on the spy installed in beforeEach by capturing it at a higher scope, or simply reference console.error since it's already mocked.

✏️ Suggested change
   it('logs error when geolocation throws an error', async () => {
     const mockError = new Error('Geolocation permission denied')
     ;(geolocationUtils.getUserLocationFromBrowser as jest.Mock).mockRejectedValue(mockError)
-    const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {})

     const ChapterMapWrapper = await getChapterMapWrapper()
     ...

     await waitFor(() => {
-      expect(consoleSpy).toHaveBeenCalledWith('Error detecting location:', mockError)
+      expect(console.error).toHaveBeenCalledWith('Error detecting location:', mockError)
       expect(queryByTestId('user-location')).not.toBeInTheDocument()
     })
   })

267-292: Test name claims to verify _distance removal, but the assertion only checks array length.

Asserting geo-loc-data-length equals '2' doesn't prove _distance was stripped. The mock ChapterMap receives geoLocData as a prop — you could inspect the actual prop values to confirm _distance is absent.

✏️ Strengthen the assertion

One approach: have the mock ChapterMap render the stringified geoLocData, then parse and check for _distance:

   // In MockChapterMap, add:
+  <span data-testid="geo-loc-data">{JSON.stringify(props.geoLocData)}</span>

   // In the test:
   await waitFor(() => {
-    expect(getByTestId('geo-loc-data-length')).toHaveTextContent('2')
+    const data = JSON.parse(getByTestId('geo-loc-data').textContent!)
+    expect(data).toHaveLength(2)
+    for (const chapter of data) {
+      expect(chapter).not.toHaveProperty('_distance')
+    }
   })

307-317: Style forwarding test doesn't actually verify the style prop value.

The test renders with customStyle but only asserts the element is in the document. It should check that the rendered chapter-map element has the expected inline styles.

✏️ Suggested fix
     await waitFor(() => {
-      expect(getByTestId('chapter-map')).toBeInTheDocument()
+      const map = getByTestId('chapter-map')
+      expect(map).toHaveStyle({ width: '500px', height: '300px' })
     })
frontend/__tests__/unit/pages/Header.test.tsx (1)

484-499: Resize test asserts nothing meaningful.

The globalThis.dispatchEvent(new Event('resize')) on line 495 doesn't reach the component's handler because addEventListener is mocked (no-op). The test ends with expect(true).toBe(true), confirming nothing about resize behavior. This gives false confidence. Consider either removing this test (the "Resize Handler" describe block already covers this properly) or restructuring it to extract and invoke the handler like the later tests do.

frontend/__tests__/unit/components/TruncatedText.test.tsx (1)

180-191: Misleading test name — doesn't verify null-ref behavior.

The test is titled "handles textRef becoming null during lifecycle" but only does a normal rerender followed by unmount. React keeps the ref populated during rerender, so this never actually tests the null path. The assertions only verify that the updated text and title render correctly, which is already covered by the "handles multiple rapid text changes" test.

Consider renaming to something like "updates correctly on rerender and unmount" to match what it actually verifies.

frontend/__tests__/unit/components/CardDetailsPage.test.tsx (1)

449-473: Avoid any casts in mock — consider a minimal type.

The Leaders mock uses as any[] and (user: any, ...) with eslint-disable comments. A simple inline type would remove the need for these suppressions and make the mock more self-documenting:

Suggested refactor
-    default: ({ users, ...props }: { users: unknown[]; [key: string]: unknown }) => {
-      // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-      const usersList = users as any[]
+    default: ({ users, ...props }: { users: Array<{ login?: string; member?: { name?: string }; memberName?: string; description?: string }>; [key: string]: unknown }) => {
+      const usersList = users
       return (
         <div data-testid="leaders" {...props}>
           <h3>Leaders</h3>
           {Array.isArray(usersList) &&
-            // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-            usersList.map((user: any, index: number) => {
+            usersList.map((user, index) => {
               const uniqueKey = `leader-${index}-${user.login || 'unknown'}`

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/SortBy.test.tsx (1)

98-122: ⚠️ Potential issue | 🔴 Critical

const orderButton is redeclared in the same block scope — this is a SyntaxError.

Lines 103 & 105 (and similarly 116 & 118) both declare const orderButton inside the same async arrow. This will crash at parse time, preventing these two tests from running. It looks like the new getByLabelText lines were added but the old buttons[1] lines were not removed.

Proposed fix — remove the stale declarations
     await act(async () => {
-      const orderButton = screen.getByLabelText(/Sort in ascending order/i)
-      const buttons = screen.getAllByRole('button')
-      const orderButton = buttons[1]
+      const orderButton = screen.getByLabelText(/Sort in ascending order/i)
       fireEvent.keyDown(orderButton, { key: 'Enter' })
     })
     expect(defaultProps.onOrderChange).toHaveBeenCalledWith('desc')
@@ -115,8 +113,6 @@
     await act(async () => {
-      const orderButton = screen.getByLabelText(/Sort in descending order/i)
-      const buttons = screen.getAllByRole('button')
-      const orderButton = buttons[1]
+      const orderButton = screen.getByLabelText(/Sort in descending order/i)
       fireEvent.keyDown(orderButton, { key: ' ' })
     })
🤖 Fix all issues with AI agents
In `@frontend/__tests__/unit/pages/CreateModule.test.tsx`:
- Around line 199-229: The test "shows access denied and redirects when user is
not an admin" currently calls jest.useFakeTimers() and then jest.useRealTimers()
at the end, which leaks fake timers if an assertion fails; fix by making timer
cleanup exception-safe: either move the test body into a try/finally and call
jest.useRealTimers() in the finally block (ensuring jest.useRealTimers() always
runs even if waitFor or expect throws), or better, remove the inline calls and
enable/restore timers via beforeEach/afterEach for this test scope; keep
references to the same mocks (useSession, useQuery, useMutation), and ensure
calls like jest.advanceTimersByTime(2000) and assertions against mockReplace
remain unchanged.
🧹 Nitpick comments (4)
frontend/__tests__/unit/pages/IssuesPage.test.tsx (1)

294-319: Consider combining the empty and null availableLabels tests with test.each.

This test is nearly identical to the one at lines 265–292, differing only in the availableLabels value (null vs []). A test.each or describe.each could reduce duplication, similar to the pattern already used for issue states at lines 194–215. This is a minor nit — perfectly fine to leave as-is.

frontend/__tests__/unit/components/SortBy.test.tsx (1)

145-159: Duplicate test — the new test (lines 145-151) asserts the same behavior as the existing test (lines 153-159).

Both tests render with selectedSortOption="default" and verify the sort-order button is absent via queryByLabelText. Remove one of them to avoid redundancy.

Proposed fix — drop the duplicate
-  it('hides sort order button when selectedSortOption is "default"', async () => {
-    await act(async () => {
-      render(<SortBy {...defaultProps} selectedSortOption="default" />)
-    })
-    const orderButtons = screen.queryByLabelText(/Sort in ascending order/i)
-    expect(orderButtons).not.toBeInTheDocument()
-  })
-
   it('does not render order button when selectedSortOption is default', async () => {
frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx (1)

502-528: Prefer Testing Library queries over document.querySelector with CSS classes.

Querying by CSS class names (rounded-full.bg-gray-400) couples these tests to styling implementation details. If classes change (e.g., Tailwind refactor), these tests break silently without any actual behavioral regression. Consider adding a data-testid="placeholder-avatar" to the placeholder element in the component, then using screen.getByTestId / screen.getAllByTestId here.

Also applies to: 546-570

frontend/__tests__/unit/pages/CreateModule.test.tsx (1)

120-127: Excessive timeouts suggest flaky test setup.

A 10 s waitFor timeout and a 15 s Jest timeout are red flags — they usually mean the mocked mutation or navigation isn't resolving synchronously as expected. Because all Apollo hooks are manually mocked, mockCreateModule should resolve in the same microtask queue, making a sub-second timeout sufficient. If these high values are needed to make the test pass, the root cause is likely that the component or mock wiring isn't firing the mutation as expected, and the test is just "timing out gracefully" rather than catching a real failure.

Consider reducing both to something like { timeout: 3000 } / 5000 and investigating why the mock doesn't resolve quickly.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 9, 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.

1 issue found across 7 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="frontend/__tests__/unit/components/FontLoaderWrapper.test.tsx">

<violation number="1" location="frontend/__tests__/unit/components/FontLoaderWrapper.test.tsx:138">
P3: This test title claims to verify `document.fonts.ready.then()` is called, but the new implementation only checks that content renders after resolving the promise. Either reintroduce an explicit `.then()` spy or rename the test to match what it actually asserts to avoid misleading coverage.</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 9, 2026
@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 9, 2026

@anurag2787 anurag2787 marked this pull request as ready for review February 9, 2026 21:33
@arkid15r arkid15r changed the title mprove frontend test coverage above 80% and add missing test files Improve frontend test coverage above 80% and add missing test files Feb 10, 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.

@anurag2787 Awesome work 👏🏼

Thank you!

@kasya kasya enabled auto-merge February 10, 2026 04:33
@codecov
Copy link

codecov bot commented Feb 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.37%. Comparing base (491a390) to head (c8f2807).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3864      +/-   ##
==========================================
+ Coverage   93.13%   95.37%   +2.23%     
==========================================
  Files         463      463              
  Lines       14531    14541      +10     
  Branches     2061     2016      -45     
==========================================
+ Hits        13533    13868     +335     
+ Misses        538      329     -209     
+ Partials      460      344     -116     
Flag Coverage Δ
backend 95.65% <ø> (ø)
frontend 94.61% <ø> (+8.32%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
frontend/src/components/RecentPullRequests.tsx 100.00% <ø> (+7.14%) ⬆️

... and 50 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 491a390...c8f2807. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kasya kasya added this pull request to the merge queue Feb 10, 2026
Merged via the queue into OWASP:main with commit 9c593c5 Feb 10, 2026
36 checks passed
arkid15r added a commit that referenced this pull request Feb 10, 2026
* Run make update

* Clean up snapshot generated videos

* Update backend/data/nest.dump

* feat(ui): revamp corporate supporters carousel (Infinite Marquee + Dark Mode fix) (#3837)

* feat(ui): revamp corporate supporters carousel (Infinite Marquee + Dark Mode fix)

* fix: resolve failing test case

* fix: add fallback text for unnamed sponsors

* docs: add docstrings to satisfy coverage requirements

* Run make check and fix tests.

---------

Co-authored-by: Kate <kate@kgthreads.com>

* Fix/redundant typescript assertion (#3834)

* Fix Sonar S4325 by narrowing session user fields instead of casting

* Fix unused ExtendedSession in mentorship page

* fix: redundant-typescript-assertion

* Fix stale latest date displayed in Project Health Dashboard metrics (#3842)

* Fixed latest date in proejct health dashboard

* updated order

* Update code

* Update code

---------

Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>

* feat: improve backend test coverage to 96% (#3840)

* feat: improve backend test coverage to 96%

* fix comments

* fix issues

* fix issue

* fix cubic-dev-ai comments

* Update code

* Fix tests

---------

Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>
Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com>

* Fix: merge consecutive RUN instructions in frontend Dockerfile (#3644)

* Fix: merge consecutive RUN instructions in frontend Dockerfile

* fix: comment Dockerfile note to prevent syntax error

* Update code

* Update code

---------

Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>
Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com>

* Fix 'is_merged' not being available on the Issue (#3843)

* Fix 'is_merged' not being available on the Issue

* Update code

---------

Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>

* CI:  Add ansible-lint workflow for Ansible playbooks (#3796)

* ci: add ansible-lint workflow

Signed-off-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>

* Update .github/workflows/lint-ansible.yaml

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* ci: add ansible-lint make target and workflow

Signed-off-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>

* ci: add ansible-lint pre-commit hook

Signed-off-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>

* fix: whitespace & version

Signed-off-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>

* Update Makefile

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* ci: enable ansible-lint scanning and add requirements.yml

Signed-off-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>

* chore(ansible):align linting and module usage

Signed-off-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>

* ci(ansible): install collections before deploy playbooks

Signed-off-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>

* Update code

* Update code

* Update .github/workflows/run-ci-cd.yaml

---------

Signed-off-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>

* Fix ElevenLabs API error (#3861)

* use default liam voice

* bump speed by 0.10

---------

Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com>

* Add Ime Iyonsi to MENTORS.md (#3866)

* Add mentor profile for Ime Iyonsi

Added Ime Iyonsi's mentor profile.

* Fix GitHub link for Ime Iyonsi

Corrected GitHub link for Ime Iyonsi.

* Update code

---------

Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>

* Update MENTORS.md

* Enabled Strict Mode (#3776)

* Enabled Strict Mode

* fixed ai review

* fix

* fixed review

* fix

* update test

* Update code

---------

Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>
Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com>

* Resolve case-sensitivity in QueryParser to support Chapters/Members search (#3844)

* resolve query parser blocker

* use case_sensitive flag in QueryParser

* feat: add case_sensitive option to QueryParser and update tests

* Update code

---------

Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>
Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com>

* Update dependencies (#3874)

* Update dependencies

* Bump django-ninja version

* fix(proxy): pin nginx and certbot images (#3848)

* fix(proxy): pin nginx and certbot images

Signed-off-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>

* fix stable verssions

Signed-off-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>

---------

Signed-off-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>
Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com>

* Update docker-compose/proxy/compose.yaml

* Update backend/pyproject.toml

* Update ansible lint configuration (#3880)

* Update .github/ansible/.ansible-lint.yaml

* Improve frontend test coverage above 80% and add missing test files (#3864)

* Imrove test coverage to 80% and added test

* Fixed coderabbit review

* update code

* fixed coderabbit ai

* fixed soanrqube warning

* fixed review

* update

* fixed aloglia cache_key (#3825)

* fixed aloglia cache_key

* change separator val to be semicolon (;)

* Update code

* add tests + use json filters

* add trailing newline

* Update code

---------

Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>
Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com>

* fix: remove unused className prop from AnchorTitle component (#3822)

* fix: remove unused className prop from AnchorTitle component

Fixes #3805

The className prop was defined in AnchorTitleProps but never used
in the component implementation. Removing it resolves Sonar rule
typescript:S6767 and improves code maintainability.

* fix: use className prop instead of removing it

- Added className back to AnchorTitleProps interface
- Accept className parameter in component
- Apply className to root div element
- Resolves reviewer feedback on PR #3822

* Update code

---------

Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>
Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com>

---------

Signed-off-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>
Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>
Co-authored-by: Yashraj Pahuja <yashrajpahuja9999@gmail.com>
Co-authored-by: Kate <kate@kgthreads.com>
Co-authored-by: CodeAritraDhank <aritradhank21@gmail.com>
Co-authored-by: Anurag Yadav <143180737+anurag2787@users.noreply.github.com>
Co-authored-by: Harshit Verma <harshit1092004@gmail.com>
Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com>
Co-authored-by: Shuban Mutagi <shubanmutagi55@gmail.com>
Co-authored-by: Muhammad Hassaan Saleem <iamhassaans@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: emaybu <152900874+emaybu@users.noreply.github.com>
Co-authored-by: sai chethana <saichethanavesireddy@gmail.com>
Co-authored-by: Rahul Paul <179798584+Mr-Rahul-Paul@users.noreply.github.com>
Co-authored-by: Lavanya <lavanyayadawad30@gmail.com>
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 test coverage of frontend files

2 participants