-
Notifications
You must be signed in to change notification settings - Fork 245
MPP-4186 - test(misc): add test coverage for Miscellaneous pages and … #6094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
4cf9457
MPP-4186 - test(misc): add test coverage for Miscellaneous pages and …
vpremamozilla 73326cc
Merge branch 'main' into MPP-4186-frontend-test-coverage-misc
vpremamozilla a6e235a
MPP-4186 - test(misc): add test coverage for Miscellaneous pages and …
vpremamozilla 8cc150c
MPP-4186 - test(misc): add test coverage for Miscellaneous pages and …
vpremamozilla 82e382c
MPP-4186 - test(misc): add test coverage for Miscellaneous pages and …
vpremamozilla 3d073d1
MPP-4186 - test(misc): add test coverage for Miscellaneous pages and …
vpremamozilla 3ca33c2
MPP-4186 - test(misc): add test coverage for Miscellaneous pages and …
vpremamozilla e7f7772
MPP-4186 - test(misc): add test coverage for Miscellaneous pages and …
vpremamozilla 756e190
MPP-4186 - test(misc): add test coverage for Miscellaneous pages and …
vpremamozilla 4857885
MPP-4186 - test(misc): add test coverage for Miscellaneous pages and …
vpremamozilla 2bf4713
MPP-4186 - test(misc): add test coverage for Miscellaneous pages and …
vpremamozilla File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| # Frontend Testing Guide | ||
|
|
||
| This document provides best practices and guidelines for writing frontend tests in the Firefox Relay project. | ||
|
|
||
| ## Table of Contents | ||
|
|
||
| - [Testing Stack](#testing-stack) | ||
| - [Quick Start](#quick-start) | ||
| - [Test File Organization](#test-file-organization) | ||
| - [Test Helper Files](#test-helper-files) | ||
| - [Writing Tests](#writing-tests) | ||
| - [Best Practices](#best-practices) | ||
| - [Common Patterns](#common-patterns) | ||
| - [Troubleshooting](#troubleshooting) | ||
|
|
||
| ## Testing Stack | ||
|
|
||
| We use the following testing tools: | ||
|
|
||
| - **Jest** (v30.2.0) - Test runner and assertion library | ||
| - **React Testing Library** (v16.3.0) - Component testing with accessibility-first queries | ||
| - **MSW (Mock Service Worker)** (v2.12.2) - API mocking | ||
| - **jest-axe** (v10.0.0) - Accessibility testing | ||
| - **@testing-library/user-event** (v14.6.1) - Realistic user interactions | ||
|
|
||
| ### Configuration Files | ||
|
|
||
| - `jest.config.js` - Jest configuration with Next.js integration | ||
| - `jest.setup.ts` - Global test setup, mocks, and utilities | ||
|
|
||
| ## Quick Start | ||
|
|
||
| ### Running Tests | ||
|
|
||
| ```bash | ||
| npm test # Run all tests | ||
| ``` | ||
|
|
||
| ### Coverage Thresholds | ||
|
|
||
| - Branches: 70% | ||
| - Functions: 70% | ||
| - Lines: 80% | ||
| - Statements: 80% | ||
|
|
||
| ## Test File Organization | ||
|
|
||
| Tests live alongside the code they test, using the `.test.tsx` or `.test.ts` extension: | ||
|
|
||
| ``` | ||
| frontend/ | ||
| src/ | ||
| components/ | ||
| Button.tsx | ||
| Button.test.tsx | ||
| hooks/ | ||
| useAliases.ts | ||
| useAliases.test.ts | ||
| pages/ | ||
| faq.page.tsx | ||
| faq.page.test.tsx | ||
| ``` | ||
|
|
||
| ## Test Helper Files | ||
|
|
||
| All test helpers are organized in the `__mocks__/` directory: | ||
|
|
||
| ### API Mocking (`__mocks__/api/`) | ||
|
|
||
| Contains MSW setup files, HTTP request handlers, and pre-defined mock data for all API endpoints and user states. | ||
|
|
||
| ### Hook Mocks (`__mocks__/hooks/`) | ||
|
|
||
| Contains mock factories for localization, API data (profile, aliases, runtime data, phone numbers, contacts, users), and related hooks. | ||
|
|
||
| ### Component Mocks (`__mocks__/components/`) | ||
|
|
||
| Contains mocks for localization components, Next.js Image component, and SVG icons. | ||
|
|
||
| ### Function Mocks (`__mocks__/functions/`) | ||
|
|
||
| Contains utilities for testing feature flags, locale functions, plan availability, and cookie handling. | ||
|
|
||
| ### Module Mocks (`__mocks__/modules/`) | ||
|
|
||
| Contains custom render utilities with providers and Next.js router mocks. | ||
|
|
||
| ## Writing Tests | ||
groovecoder marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| - Use `render` and `screen` from `@testing-library/react` for basic component tests | ||
| - If your component needs providers (localization, overlay provider), use `renderWithProviders` from `__mocks__/modules/renderWithProviders` | ||
| - Always use `userEvent` for realistic user interactions (clicks, typing, etc.) and remember to `await` all userEvent calls | ||
| - Use `renderHook` from React Testing Library for testing custom hooks | ||
| - Use mock factory functions (`setMockProfileData`, `getMockRuntimeData`, `getMockRandomAlias`, etc.) for consistent, configurable API data | ||
| - Use `*Once()` methods (e.g., `setMockAliasesDataOnce`) when you need different mock data for a single test | ||
| - Use `setFlags`, `resetFlags`, and `withFlag` from `__mocks__/functions/flags` for testing feature flag dependent components | ||
| - Use `byMsgId` from `__mocks__/hooks/l10n` for testing localized content | ||
| - Every component should have an accessibility test using `axe` from `jest-axe` | ||
|
|
||
| ## Best Practices | ||
groovecoder marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| - Use accessibility-first queries (e.g., `getByRole`, `getByLabelText`) that reflect how users interact with your app instead of implementation details like test IDs or class names | ||
| - Use mock factories (e.g., `getMockRandomAlias`, `setMockProfileData`) instead of hardcoded data for configurable, maintainable tests | ||
| - Reset state between tests using `beforeEach` (e.g., `resetFlags()`). Jest automatically clears all mocks between tests | ||
| - Test user flows and behavior, not implementation details like function calls or internal state | ||
| - Use `waitFor` from `@testing-library/react` for async behavior | ||
| - Keep tests focused with one assertion per test when possible, and use descriptive test names | ||
| - Don't test external libraries (React Testing Library, Next.js, etc.). Trust they work and test your code's behavior | ||
|
|
||
| ## Common Patterns | ||
groovecoder marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Search the codebase for real examples of these patterns: | ||
|
|
||
| - **Forms**: Use `userEvent.type()` for inputs, `userEvent.click()` for submission, and assert the handler was called with expected data | ||
| - **Error States**: Use `setMock*DataOnce(null)` to simulate fetch failures, then assert error messages appear with `waitFor` | ||
| - **Loading States**: Assert loading indicators appear using `getByRole("status")` | ||
| - **Conditional Rendering**: Set mock data with different states and use `getBy*` for expected elements, `queryBy*` with `.not.toBeInTheDocument()` for hidden elements | ||
| - **Lists**: Create multiple mock items with factory functions, set mock data, and assert each item appears in the document | ||
|
|
||
| ## Troubleshooting | ||
groovecoder marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ### Test Fails with "Not wrapped in act(...)" | ||
|
|
||
| Use `waitFor` or `await userEvent.*` for async operations: | ||
|
|
||
| ```typescript | ||
| // Before | ||
| userEvent.click(button); // Missing await | ||
|
|
||
| // After | ||
| await userEvent.click(button); | ||
| ``` | ||
|
|
||
| ### Mock Data Not Applied | ||
|
|
||
| Make sure you're calling `setMock*` functions before rendering: | ||
|
|
||
| ```typescript | ||
| // Wrong order | ||
| render(<Component />); | ||
| setMockProfileData({ has_premium: true }); // Too late! | ||
|
|
||
| // Correct order | ||
| setMockProfileData({ has_premium: true }); | ||
| render(<Component />); | ||
| ``` | ||
|
|
||
| ### Element Not Found | ||
|
|
||
| 1. Check if element is rendered conditionally | ||
| 2. Use `findBy*` for async elements | ||
| 3. Use `queryBy*` to assert non-existence | ||
| 4. Use `screen.debug()` to see current DOM | ||
|
|
||
| ```typescript | ||
| screen.debug(); // Prints current DOM to console | ||
| ``` | ||
|
|
||
| ### Feature Flag Not Working | ||
|
|
||
| Reset flags in `beforeEach`: | ||
|
|
||
| ```typescript | ||
| beforeEach(() => { | ||
| resetFlags(); | ||
| setFlags({ my_flag: true }); | ||
| }); | ||
| ``` | ||
|
|
||
| ### Type Errors with Mock Data | ||
|
|
||
| Use the factory functions to get properly typed mock data: | ||
|
|
||
| ```typescript | ||
| // Good - typed correctly | ||
| const alias = getMockRandomAlias(); | ||
|
|
||
| // Avoid - may have type errors | ||
| const alias = { address: "[email protected]" }; // Missing required fields | ||
| ``` | ||
|
|
||
| ## Additional Resources | ||
|
|
||
| - [React Testing Library Documentation](https://testing-library.com/react) | ||
| - [Jest Documentation](https://jestjs.io/) | ||
| - [MSW Documentation](https://mswjs.io/) | ||
| - [jest-axe Documentation](https://github.com/nickcolley/jest-axe) | ||
|
|
||
| ## Contributing | ||
|
|
||
| When adding new features: | ||
|
|
||
| 1. Write tests alongside your code | ||
| 2. Use existing mock patterns for consistency | ||
| 3. Add new mock factories to `__mocks__/` when needed | ||
| 4. Run tests before committing: `npm test` | ||
| 5. Ensure coverage thresholds are met | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| // This file is used by Jest's automatic mock resolution | ||
| // When tests call jest.mock("../../config"), Jest finds this file automatically | ||
| export const { getRuntimeConfig } = jest.requireActual( | ||
| "./configMock", | ||
| ).mockConfigModule; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| /** | ||
| * Common test helper functions to reduce duplication across test files | ||
| */ | ||
|
|
||
| /** | ||
| * Mock the useFirstSeen hook to return a date X days ago | ||
| * @param daysAgo - Number of days ago (e.g., 7 for 7 days ago) | ||
| */ | ||
| export function mockFirstSeenDaysAgo(daysAgo: number): void { | ||
| const useFirstSeen = | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (jest.requireMock("../src/hooks/firstSeen.ts") as any).useFirstSeen; | ||
| useFirstSeen.mockReturnValue( | ||
| new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000), | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Mock the useFirstSeen hook to return a specific date | ||
| * @param date - The date to return | ||
| */ | ||
| export function mockFirstSeen(date: Date | null): void { | ||
| const useFirstSeen = | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (jest.requireMock("../src/hooks/firstSeen.ts") as any).useFirstSeen; | ||
| useFirstSeen.mockReturnValue(date); | ||
| } | ||
|
|
||
| /** | ||
| * Mock the useFirstSeen hook to return a date X days ago (one-time mock) | ||
| * @param daysAgo - Number of days ago | ||
| */ | ||
| export function mockFirstSeenDaysAgoOnce(daysAgo: number): void { | ||
| const useFirstSeen = | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (jest.requireMock("../src/hooks/firstSeen.ts") as any).useFirstSeen; | ||
| useFirstSeen.mockReturnValueOnce( | ||
| new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000), | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Mock cookie dismissal for a specific survey/feature | ||
| * @param cookieKey - The cookie key to check (e.g., "free-7days", "premium-oneyear") | ||
| */ | ||
| export function mockCookieDismissal(cookieKey: string): void { | ||
| const getCookie: jest.Mock = | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (jest.requireMock("../src/functions/cookies.ts") as any).getCookie; | ||
| getCookie.mockImplementation((key: string) => | ||
| key.includes(cookieKey) ? Date.now() : undefined, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Mock useLocalDismissal hook | ||
| * @param isDismissed - Whether the component is dismissed | ||
| * @param dismissFn - Optional custom dismiss function | ||
| */ | ||
| export function mockLocalDismissal( | ||
| isDismissed: boolean, | ||
| dismissFn?: jest.Mock, | ||
| ): void { | ||
| const useLocalDismissal = | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (jest.requireMock("../src/hooks/localDismissal.ts") as any) | ||
| .useLocalDismissal; | ||
| useLocalDismissal.mockReturnValue({ | ||
| isDismissed, | ||
| dismiss: dismissFn || jest.fn(), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Mock the useIsLoggedIn hook | ||
| * @param status - Login status ("logged-in", "logged-out", or "unknown") | ||
| */ | ||
| export function mockLoginStatus( | ||
| status: "logged-in" | "logged-out" | "unknown", | ||
| ): void { | ||
| const useIsLoggedIn = | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (jest.requireMock("../src/hooks/session.ts") as any).useIsLoggedIn; | ||
| useIsLoggedIn.mockReturnValue(status); | ||
| } | ||
|
|
||
| /** | ||
| * Get the mocked dismiss function from useLocalDismissal | ||
| */ | ||
| export function getMockedDismissFn(): jest.Mock { | ||
| const useLocalDismissal = | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (jest.requireMock("../src/hooks/localDismissal.ts") as any) | ||
| .useLocalDismissal; | ||
| return useLocalDismissal.mock.results[ | ||
| useLocalDismissal.mock.results.length - 1 | ||
| ].value.dismiss; | ||
| } | ||
|
|
||
| /** | ||
| * Check that multiple l10n strings are present in the document | ||
| * @param screen - The screen object from @testing-library/react | ||
| * @param strings - Array of l10n string keys to check | ||
| */ | ||
| export function expectL10nStrings(screen: any, strings: string[]): void { | ||
| strings.forEach((str) => { | ||
| expect( | ||
| screen.getByText(`l10n string: [${str}], with vars: {}`), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note (non-blocking): when we merge this, I'll refactor the agent instructions to incorporate this too.