diff --git a/frontend/README.md b/frontend/README.md index 5a6d1fc6..317d9a3d 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -33,3 +33,39 @@ docker-compose build frontend docker-compose up ``` + +## Guide for Jest Testing + +Two kinds of tests are implemented using Jest: + +### Rendering Tests + +Run rendering tests with the following command: + +```bash +npm run test:without-snapshot +``` + +> **Note:** The output may show yellow lines marking snapshots as obsolete. This can be safely ignored. + +### Snapshot Tests + +Run snapshot tests with: + +```bash +npm run test:snapshot +``` + +If one of the tests fails, verify whether the component change was intentional. If so, update the snapshot using: + +```bash +npm run test:update-snapshot +``` + +### Running All Tests + +Both test types can be run simultaneously: + +```bash +npm run test +``` diff --git a/frontend/jest.config.cjs b/frontend/jest.config.cjs index 79dba1ee..dab1cf45 100644 --- a/frontend/jest.config.cjs +++ b/frontend/jest.config.cjs @@ -1,3 +1,6 @@ +// Activer le serializer Radix pour tous les tests +process.env.RADIX_SNAPSHOT = '1'; + module.exports = { preset: 'ts-jest', testEnvironment: 'jsdom', diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts index 7b0828bf..dcc614ba 100644 --- a/frontend/jest.setup.ts +++ b/frontend/jest.setup.ts @@ -1 +1,53 @@ import '@testing-library/jest-dom'; +import { expect } from '@jest/globals'; +import type { Plugin } from 'pretty-format'; + +let isSerializing = false; + +const radixSnapshotSerializer: Plugin = { + test(val) { + return ( + !isSerializing && + val && + typeof val === 'object' && + 'nodeType' in (val as any) && + ((val as any).nodeType === 1 || (val as any).nodeType === 11) + ); + }, + print(val, serialize) { + const clone = (val as any).cloneNode(true); + + if (clone && typeof (clone as any).querySelectorAll === 'function') { + const elements = (clone as any).querySelectorAll( + '[id],[aria-controls],[aria-labelledby]' + ); + + elements.forEach((element: Element) => { + const id = element.getAttribute('id'); + if (id && id.startsWith('radix-:r')) { + element.setAttribute('id', 'radix-:ID:'); + } + + const ariaControls = element.getAttribute('aria-controls'); + if (ariaControls && ariaControls.startsWith('radix-:r')) { + element.setAttribute('aria-controls', 'radix-:ID:'); + } + + const ariaLabelledby = element.getAttribute('aria-labelledby'); + if (ariaLabelledby && ariaLabelledby.startsWith('radix-:r')) { + element.setAttribute('aria-labelledby', 'radix-:ID:'); + } + }); + } + + isSerializing = true; + const result = serialize(clone); + isSerializing = false; + return result; + }, +}; + +// Serializer Radix actif uniquement pour les snapshots (quand RADIX_SNAPSHOT=1) +if (process.env.RADIX_SNAPSHOT) { + expect.addSnapshotSerializer(radixSnapshotSerializer); +} diff --git a/frontend/package.json b/frontend/package.json index 6e9efecf..d38ae371 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -407,6 +407,9 @@ "dev": "vite", "build": "tsc && vite build", "test": "jest", + "test:without-snapshot": "jest --testNamePattern=\"^(?!.*snapshot).*$\"", + "test:snapshot": "jest --testNamePattern='snapshot'", + "test:update-snapshot": " jest -u --testNamePattern='snapshot'", "coverage": "jest --coverage", "postinstall": "git config core.hooksPath .githooks", "lint": "npx prettier --write .", diff --git a/frontend/src/components/HomeComponents/BottomBar/__tests__/BottomBar.test.tsx b/frontend/src/components/HomeComponents/BottomBar/__tests__/BottomBar.test.tsx index 44878bac..60d7e6fb 100644 --- a/frontend/src/components/HomeComponents/BottomBar/__tests__/BottomBar.test.tsx +++ b/frontend/src/components/HomeComponents/BottomBar/__tests__/BottomBar.test.tsx @@ -82,3 +82,41 @@ describe('BottomBar Component', () => { expect(screen.getByTestId('multiselect-count-tags')).toHaveTextContent('1'); }); }); + +describe('BottomBar Component using Snapshot', () => { + test('renders correctly without selected props', () => { + const mockNoSelectedProps: BottomBarProps = { + ...mockProps, + selectedProjects: [], + selectedStatuses: [], + selectedTags: [], + }; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot('Bottom bar without selected props'); + }); + test('renders correctly with only one selected props', () => { + const mockOnlyOneSelectedProps: BottomBarProps = { + ...mockProps, + selectedProjects: ['Project A'], + selectedStatuses: ['pending'], + selectedTags: ['tag1'], + }; + + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot( + 'Bottom bar with only one selected props' + ); + }); + test('renders correctly with several selected props', () => { + const mockSeveralSelectedProps: BottomBarProps = { + ...mockProps, + selectedProjects: ['Project A', 'Project B'], + selectedStatuses: ['pending', 'completed'], + selectedTags: ['tag1', 'tag2', 'tag3'], + }; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot( + 'Bottom bar with several selected props' + ); + }); +}); diff --git a/frontend/src/components/HomeComponents/BottomBar/__tests__/__snapshots__/BottomBar.test.tsx.snap b/frontend/src/components/HomeComponents/BottomBar/__tests__/__snapshots__/BottomBar.test.tsx.snap new file mode 100644 index 00000000..dcfc0ec0 --- /dev/null +++ b/frontend/src/components/HomeComponents/BottomBar/__tests__/__snapshots__/BottomBar.test.tsx.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BottomBar Component using Snapshot renders correctly with only one selected props: Bottom bar with only one selected props 1`] = ` + +
+ +
+
+`; + +exports[`BottomBar Component using Snapshot renders correctly with several selected props: Bottom bar with several selected props 1`] = ` + +
+ +
+
+`; + +exports[`BottomBar Component using Snapshot renders correctly without selected props: Bottom bar without selected props 1`] = ` + +
+ +
+
+`; diff --git a/frontend/src/components/HomeComponents/DevLogs/__tests__/DevLogs.test.tsx b/frontend/src/components/HomeComponents/DevLogs/__tests__/DevLogs.test.tsx new file mode 100644 index 00000000..5487031d --- /dev/null +++ b/frontend/src/components/HomeComponents/DevLogs/__tests__/DevLogs.test.tsx @@ -0,0 +1,141 @@ +import { render, waitFor, screen } from '@testing-library/react'; +import { DevLogs } from '../DevLogs'; + +// Mock UI components +jest.mock('../../../ui/dialog', () => ({ + Dialog: ({ children, open }: any) => (open ?
{children}
: null), + DialogContent: ({ children }: any) =>
{children}
, + DialogDescription: ({ children }: any) =>
{children}
, + DialogHeader: ({ children }: any) =>
{children}
, + DialogTitle: ({ children }: any) =>
{children}
, +})); + +jest.mock('../../../ui/button', () => ({ + Button: ({ children, ...props }: any) => ( + + ), +})); + +jest.mock('../../../ui/select', () => ({ + Select: ({ children }: any) =>
{children}
, + SelectContent: ({ children }: any) =>
{children}
, + SelectItem: ({ children }: any) =>
{children}
, + SelectTrigger: ({ children }: any) =>
{children}
, + SelectValue: ({ placeholder }: any) =>
{placeholder}
, +})); + +jest.mock('lucide-react', () => ({ + CopyIcon: () =>
CopyIcon
, + CheckIcon: () =>
CheckIcon
, +})); + +jest.mock('../../../utils/URLs', () => ({ + url: { + backendURL: 'http://mocked-backend-url/', + }, +})); + +jest.mock('react-toastify', () => ({ + toast: { + error: jest.fn(), + success: jest.fn(), + }, +})); + +jest.spyOn(Date.prototype, 'toLocaleString').mockImplementation(function ( + this: Date +) { + return this.toUTCString(); +}); + +Object.assign(navigator, { + clipboard: { + writeText: jest.fn(), + }, +}); + +const mockLogs = [ + { + timestamp: '2024-01-01T12:00:00Z', + level: 'INFO', + message: 'Sync operation started', + syncId: 'sync-123', + operation: 'SYNC_START', + }, + { + timestamp: '2024-01-01T12:01:00Z', + level: 'WARN', + message: 'Warning message', + }, + { + timestamp: '2024-01-01T12:02:00Z', + level: 'ERROR', + message: 'Error occurred', + operation: 'SYNC_ERROR', + }, +]; + +global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockLogs), + }) +) as jest.Mock; + +describe('DevLogs Component using Snapshot', () => { + const mockOnOpenChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders closed dialog correctly', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot('devlogs-closed'); + }); + + it('renders open dialog with logs correctly', async () => { + const { asFragment } = render( + + ); + + await waitFor(() => { + expect(screen.queryByText('Loading logs...')).not.toBeInTheDocument(); + }); + + expect(asFragment()).toMatchSnapshot('devlogs-with-logs'); + }); + + it('renders loading state correctly', () => { + (fetch as jest.Mock).mockImplementationOnce( + () => new Promise(() => {}) // Never resolves to keep loading state + ); + + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot('devlogs-loading'); + }); + + it('renders empty logs state correctly', async () => { + (fetch as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve([]), + }) + ); + + const { asFragment } = render( + + ); + + await waitFor(() => { + expect(screen.getByText('No logs available')).toBeInTheDocument(); + }); + + expect(asFragment()).toMatchSnapshot('devlogs-empty'); + }); +}); diff --git a/frontend/src/components/HomeComponents/DevLogs/__tests__/__snapshots__/DevLogs.test.tsx.snap b/frontend/src/components/HomeComponents/DevLogs/__tests__/__snapshots__/DevLogs.test.tsx.snap new file mode 100644 index 00000000..6d1656d8 --- /dev/null +++ b/frontend/src/components/HomeComponents/DevLogs/__tests__/__snapshots__/DevLogs.test.tsx.snap @@ -0,0 +1,332 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DevLogs Component using Snapshot renders closed dialog correctly: devlogs-closed 1`] = ``; + +exports[`DevLogs Component using Snapshot renders empty logs state correctly: devlogs-empty 1`] = ` + +
+
+
+
+ Developer Logs +
+
+ View sync operation logs with timestamps and status information. +
+
+
+
+
+
+ Filter by level +
+
+
+
+ All Levels +
+
+ INFO +
+
+ WARN +
+
+ ERROR +
+
+
+
+ + +
+
+
+
+ No logs available +
+
+
+
+
+`; + +exports[`DevLogs Component using Snapshot renders loading state correctly: devlogs-loading 1`] = ` + +
+
+
+
+ Developer Logs +
+
+ View sync operation logs with timestamps and status information. +
+
+
+
+
+
+ Filter by level +
+
+
+
+ All Levels +
+
+ INFO +
+
+ WARN +
+
+ ERROR +
+
+
+
+ + +
+
+
+
+ Loading logs... +
+
+
+
+
+`; + +exports[`DevLogs Component using Snapshot renders open dialog with logs correctly: devlogs-with-logs 1`] = ` + +
+
+
+
+ Developer Logs +
+
+ View sync operation logs with timestamps and status information. +
+
+
+
+
+
+ Filter by level +
+
+
+
+ All Levels +
+
+ INFO +
+
+ WARN +
+
+ ERROR +
+
+
+
+ + +
+
+
+
+
+
+
+
+ + Mon, 01 Jan 2024 12:00:00 GMT + + + [INFO] + + + SYNC_START + +
+
+ Sync operation started +
+
+ Sync ID: sync-123 +
+
+ +
+
+
+
+
+
+ + Mon, 01 Jan 2024 12:01:00 GMT + + + [WARN] + +
+
+ Warning message +
+
+ +
+
+
+
+
+
+ + Mon, 01 Jan 2024 12:02:00 GMT + + + [ERROR] + + + SYNC_ERROR + +
+
+ Error occurred +
+
+ +
+
+
+
+
+
+
+`; diff --git a/frontend/src/components/HomeComponents/FAQ/__tests__/FAQ.test.tsx b/frontend/src/components/HomeComponents/FAQ/__tests__/FAQ.test.tsx index 1ba582f5..5cee5916 100644 --- a/frontend/src/components/HomeComponents/FAQ/__tests__/FAQ.test.tsx +++ b/frontend/src/components/HomeComponents/FAQ/__tests__/FAQ.test.tsx @@ -60,3 +60,10 @@ describe('FAQ component', () => { expect(contactLink).toHaveAttribute('href', '#contact'); }); }); + +describe('FAQ component using snapshot', () => { + test('renders correctly', () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/components/HomeComponents/FAQ/__tests__/FAQItem.test.tsx b/frontend/src/components/HomeComponents/FAQ/__tests__/FAQItem.test.tsx index 65b5f258..e0ba10ee 100644 --- a/frontend/src/components/HomeComponents/FAQ/__tests__/FAQItem.test.tsx +++ b/frontend/src/components/HomeComponents/FAQ/__tests__/FAQItem.test.tsx @@ -2,11 +2,11 @@ import { render, screen, fireEvent } from '@testing-library/react'; import { FAQItem } from '../FAQItem'; import { Accordion } from '@/components/ui/accordion'; -describe('FAQItem', () => { - const question = 'What is your return policy?'; - const answer = 'You can return any item within 30 days of purchase.'; - const value = 'faq-1'; +const question = 'What is your return policy?'; +const answer = 'You can return any item within 30 days of purchase.'; +const value = 'faq-1'; +describe('FAQItem', () => { test('renders question and answer correctly', () => { render( @@ -29,3 +29,14 @@ describe('FAQItem', () => { expect(screen.getByText(answer)).toBeVisible(); }); }); + +describe('FAQItem using Snapshot', () => { + test('renders correctly', () => { + const { asFragment } = render( + + + + ); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/components/HomeComponents/FAQ/__tests__/__snapshots__/FAQ.test.tsx.snap b/frontend/src/components/HomeComponents/FAQ/__tests__/__snapshots__/FAQ.test.tsx.snap new file mode 100644 index 00000000..679b5800 --- /dev/null +++ b/frontend/src/components/HomeComponents/FAQ/__tests__/__snapshots__/FAQ.test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FAQ component using snapshot renders correctly 1`] = ` + +
+

+ Frequently Asked + + Questions + +

+
+
+

+ What is React? +

+

+ A JavaScript library for building user interfaces. +

+
+
+

+ What is TypeScript? +

+

+ A typed superset of JavaScript that compiles to plain JavaScript. +

+
+
+

+ Still have questions? + + Contact us + + +

+
+
+`; diff --git a/frontend/src/components/HomeComponents/FAQ/__tests__/__snapshots__/FAQItem.test.tsx.snap b/frontend/src/components/HomeComponents/FAQ/__tests__/__snapshots__/FAQItem.test.tsx.snap new file mode 100644 index 00000000..a5e8b2a3 --- /dev/null +++ b/frontend/src/components/HomeComponents/FAQ/__tests__/__snapshots__/FAQItem.test.tsx.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FAQItem using Snapshot renders correctly 1`] = ` + +
+
+

+ +

+ +
+ +`; diff --git a/frontend/src/components/HomeComponents/Footer/__tests__/Footer.test.tsx b/frontend/src/components/HomeComponents/Footer/__tests__/Footer.test.tsx index 47e2fbe0..d4c454fa 100644 --- a/frontend/src/components/HomeComponents/Footer/__tests__/Footer.test.tsx +++ b/frontend/src/components/HomeComponents/Footer/__tests__/Footer.test.tsx @@ -22,3 +22,10 @@ describe('Footer component', () => { expect(logoElement).toBeInTheDocument(); }); }); + +describe('Footer component using Snapshot', () => { + test('renders correctly', () => { + const { asFragment } = render(