Skip to content
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

feat: mise en place de TU avec react testing library + quelques refacto sur les typages #2421

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ services:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: egapro
ports:
- 5437:5432
- 5438:5432
volumes:
- pgdata:/var/lib/postgresql/data

Expand Down
18 changes: 15 additions & 3 deletions packages/app/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,22 @@ const createJestConfig = nextJest({
// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
// Add more setup options before each test is run
// setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],

testEnvironment: "jest-environment-jsdom",
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
testMatch: ["**/__tests__/**/*?(*.)+(test|spec).[jt]s?(x)"],
testPathIgnorePatterns: ["<rootDir>/.next/", "<rootDir>/node_modules/", "<rootDir>/cypress/"],
testTimeout: 20000,
moduleNameMapper: {
"(../){0,}design-system/@design-system": "<rootDir>/src/design-system/server.ts",
"@components/utils/(.*)$": "<rootDir>/src/components/utils/$1",
"@common/(.*)$": "<rootDir>/src/common/$1",
"@api/(.*)$": "<rootDir>/src/api/$1",
"@services/(.*)$": "<rootDir>/src/services/$1",
"@design-system/utils/(.*)$": "<rootDir>/src/design-system/utils/$1",
"@design-system/hooks/(.*)$": "<rootDir>/src/design-system/hooks/$1",
"@public/(.*)$": "<rootDir>/src/public/$1",
"@globalActions/(.*)$": "<rootDir>/src/globalActions/$1",
},
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
Expand Down
29 changes: 29 additions & 0 deletions packages/app/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import "@testing-library/jest-dom";

import { expect, jest } from "@jest/globals";
import { screen } from "@testing-library/react";

global.jest = jest;
global.expect = expect;
global.screen = screen;
global.TextEncoder = require("util").TextEncoder;
global.TextDecoder = require("util").TextDecoder;

import * as mockRouter from "next-router-mock";

const useRouter = mockRouter.useRouter;

jest.mock("next/navigation", () => ({
...mockRouter,
useSearchParams: () => {
const router = useRouter();
const path = router.query;
return new URLSearchParams(path);
},
usePathname: jest.fn(),
redirect: jest.fn(),
}));

jest.mock("@formkit/auto-animate/react", () => ({
useAutoAnimate: jest.fn(() => []),
}));
12 changes: 8 additions & 4 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"lint": "next lint",
"lint:fix": "next lint --fix",
"tsc": "node_modules/.bin/tsc --pretty --noEmit",
"test": "jest --watch",
"test": "jest --silent",
"test:watch": "jest --watch --silent",
"type-check": "tsc",
"generateEnvDeclaration": "ts-node --project scripts/tsconfig.json scripts/generateEnvDeclaration.ts",
"generateModelsFromSchema": "ts-node --project scripts/tsconfig.json scripts/generateModelsFromSchema.ts",
Expand Down Expand Up @@ -72,13 +73,15 @@
"@faker-js/faker": "^8.1.0",
"@hookform/devtools": "^4.3.0",
"@next/eslint-plugin-next": "14.0.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.1.2",
"@testing-library/dom": "^8.11.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@tsconfig/next": "^2.0.0",
"@tsconfig/node16": "^16.1.0",
"@types/cors": "^2.8.13",
"@types/dotenv": "^8.2.0",
"@types/jest": "^29.5.9",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.3",
"@types/mime": "^3.0.1",
"@types/node": "^20.7.0",
Expand All @@ -105,6 +108,7 @@
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"json-schema-to-typescript": "^12.0.0",
"next-router-mock": "^0.9.13",
"prettier": "^3.0.0",
"sass": "^1.68.0",
"ts-node": "^10.9.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,28 @@ export const egaproNextAuthAdapter: Adapter = {
},

async createVerificationToken(verificationToken: VerificationToken): Promise<VerificationToken | null | undefined> {
tokenCache.set(verificationToken.identifier, verificationToken);
return verificationToken;
const cleanIdentifier = verificationToken.identifier.trim();
const cleanToken = {
...verificationToken,
identifier: cleanIdentifier,
};

tokenCache.set(cleanIdentifier, cleanToken);
return cleanToken;
},

/**
* Return verification token from the database
* and delete it so it cannot be used again.
*/
async useVerificationToken({ identifier, token }: SentVerificationToken): Promise<VerificationToken | null> {
const foundToken = tokenCache.get(identifier);
const cleanIdentifier = identifier.trim();

const foundToken = tokenCache.get(cleanIdentifier);
if (foundToken?.token === token) {
tokenCache.delete(identifier);
tokenCache.delete(cleanIdentifier);
return foundToken;
}

return null;
},
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { logger } from "@api/utils/pino";
import { type OAuthConfig, type OAuthUserConfig } from "next-auth/providers";
import { type OAuthConfig, type OAuthUserConfig } from "next-auth/providers/oauth";

export interface Organization {
id: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { isEqual } from "date-fns";
import { BaseReceiptTemplate, type BaseReceiptTemplateProps } from "./BaseReceiptTemplate";

const insertSoftHyphens = (url: string, everyNChars: number) => {
const parts = [];
const parts: string[] = [];
for (let i = 0; i < url.length; i += everyNChars) {
parts.push(url.substring(i, i + everyNChars));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { isEqual } from "date-fns";
import { BaseReceiptTemplate, type BaseReceiptTemplateProps } from "./BaseReceiptTemplate";

const insertSoftHyphens = (url: string, everyNChars: number) => {
const parts = [];
const parts: string[] = [];
for (let i = 0; i < url.length; i += everyNChars) {
parts.push(url.substring(i, i + everyNChars));
}
Expand Down
131 changes: 131 additions & 0 deletions packages/app/src/app/(default)/__tests__/Navigation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { render, screen } from "@testing-library/react";
import { useSelectedLayoutSegment, useSelectedLayoutSegments } from "next/navigation";
import { useSession } from "next-auth/react";

import { Navigation } from "../Navigation";

// Mock next/navigation
jest.mock("next/navigation", () => ({
useSelectedLayoutSegment: jest.fn(),
useSelectedLayoutSegments: jest.fn(),
}));

// Mock next-auth/react
jest.mock("next-auth/react", () => ({
useSession: jest.fn(),
}));

interface MenuItem {
linkProps?: {
href: string;
};
menuLinks?: Array<{
linkProps: {
href: string;
};
text: string;
}>;
text: string;
}

// Mock MainNavigation from react-dsfr
jest.mock("@codegouvfr/react-dsfr/MainNavigation", () => ({
MainNavigation: ({ items }: { items: MenuItem[] }) => (
<nav>
{items.map((item, index) => (
<div key={index} data-testid="nav-item">
<span>{item.text}</span>
{item.menuLinks?.map((link, linkIndex) => (
<a
key={linkIndex}
href={link.linkProps.href}
data-testid={`menu-link-${link.text.toLowerCase().replace(/\s+/g, "-")}`}
>
{link.text}
</a>
))}
</div>
))}
</nav>
),
}));

// Mock admin menu items
jest.mock("../../admin/Navigation", () => ({
adminMenuItems: [
{ text: "Admin Item 1", href: "/admin/1" },
{ text: "Admin Item 2", href: "/admin/2" },
],
}));

describe("<Navigation />", () => {
const mockUseSession = useSession as jest.Mock;
const mockUseSelectedLayoutSegment = useSelectedLayoutSegment as jest.Mock;
const mockUseSelectedLayoutSegments = useSelectedLayoutSegments as jest.Mock;

beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();

// Default mock implementations
mockUseSession.mockReturnValue({ data: { user: { staff: false }, staff: { impersonating: false } } });
mockUseSelectedLayoutSegment.mockReturnValue(null);
mockUseSelectedLayoutSegments.mockReturnValue([]);
});

it("should render all main navigation items", () => {
render(<Navigation />);

const navItems = screen.getAllByTestId("nav-item");
expect(navItems).toHaveLength(3); // Accueil, Index, Représentation équilibrée

expect(screen.getByText("Accueil")).toBeInTheDocument();
expect(screen.getByText("Index")).toBeInTheDocument();
expect(screen.getByText("Représentation équilibrée")).toBeInTheDocument();
});

it("should render admin menu when user is staff", () => {
mockUseSession.mockReturnValue({ data: { user: { staff: true }, staff: { impersonating: false } } });
render(<Navigation />);

const navItems = screen.getAllByTestId("nav-item");
expect(navItems).toHaveLength(4); // Including Admin menu
expect(screen.getByText("Admin")).toBeInTheDocument();
});

it("should render admin menu when user is impersonating", () => {
mockUseSession.mockReturnValue({ data: { user: { staff: false }, staff: { impersonating: true } } });
render(<Navigation />);

expect(screen.getByText("Admin")).toBeInTheDocument();
});

it("should render index submenu with correct links", () => {
render(<Navigation />);

expect(screen.getByTestId("menu-link-à-propos-de-l'index")).toHaveAttribute("href", "/index-egapro");
expect(screen.getByTestId("menu-link-calculer-mon-index")).toHaveAttribute(
"href",
"/index-egapro/simulateur/commencer",
);
expect(screen.getByTestId("menu-link-déclarer-mon-index")).toHaveAttribute(
"href",
"/index-egapro/declaration/assujetti",
);
expect(screen.getByTestId("menu-link-consulter-l'index")).toHaveAttribute("href", "/index-egapro/recherche");
});

it("should render representation equilibree submenu with correct links", () => {
render(<Navigation />);

expect(screen.getByTestId("menu-link-à-propos-des-écarts")).toHaveAttribute("href", "/representation-equilibree");
expect(screen.getByTestId("menu-link-déclarer-les-écarts")).toHaveAttribute(
"href",
"/representation-equilibree/assujetti",
);
expect(screen.getByTestId("menu-link-consulter-les-écarts")).toHaveAttribute(
"href",
"/representation-equilibree/recherche",
);
});
});
69 changes: 69 additions & 0 deletions packages/app/src/app/(default)/__tests__/layout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { render, screen } from "@testing-library/react";
import { type ReactNode } from "react";

import DefaultLayout from "../layout";

// Mock the imported components
jest.mock("../../Footer", () => ({
Footer: () => <footer data-testid="footer">Footer</footer>,
}));

jest.mock("../../Header", () => ({
Header: ({ auth, navigation }: { auth: boolean; navigation: ReactNode }) => (
<header data-testid="header">
Header
{auth && <div data-testid="auth">Auth enabled</div>}
{navigation}
</header>
),
}));

jest.mock("../Navigation", () => ({
Navigation: () => <nav data-testid="navigation">Navigation</nav>,
}));

// Mock the CSS module
jest.mock("../default.module.css", () => ({
app: "app-class",
content: "content-class",
}));

describe("<DefaultLayout />", () => {
it("should render the complete layout structure", async () => {
const TestChild = () => <div data-testid="test-child">Test Content</div>;
render(await DefaultLayout({ children: <TestChild /> }));

// Check main structure elements
expect(screen.getByTestId("header")).toBeInTheDocument();
expect(screen.getByRole("main")).toBeInTheDocument();
expect(screen.getByTestId("footer")).toBeInTheDocument();
});

it("should render the navigation component in header", async () => {
render(await DefaultLayout({ children: null }));

expect(screen.getByTestId("navigation")).toBeInTheDocument();
expect(screen.getByTestId("header")).toContainElement(screen.getByTestId("navigation"));
});

it("should render children in main content area", async () => {
const TestChild = () => <div data-testid="test-child">Test Content</div>;
render(await DefaultLayout({ children: <TestChild /> }));

const main = screen.getByRole("main");
expect(main).toContainElement(screen.getByTestId("test-child"));
});

it("should apply correct CSS classes", async () => {
render(await DefaultLayout({ children: null }));

expect(screen.getByTestId("header").parentElement).toHaveClass("app-class");
expect(screen.getByRole("main")).toHaveClass("content-class");
});

it("should enable auth in header", async () => {
render(await DefaultLayout({ children: null }));

expect(screen.getByTestId("auth")).toBeInTheDocument();
});
});
22 changes: 22 additions & 0 deletions packages/app/src/app/(default)/__tests__/messages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NOT_BELOW_0, NOT_BELOW_N_RESULT, NOT_HIGHER_THAN_N_RESULT } from "../messages";

describe("Messages", () => {
describe("Dynamic messages", () => {
it("should generate correct not below N message", () => {
expect(NOT_BELOW_N_RESULT(5)).toBe("Le résultat ne peut pas être inférieur à 5");
expect(NOT_BELOW_N_RESULT(10)).toBe("Le résultat ne peut pas être inférieur à 10");
});

it("should generate correct not higher than N message", () => {
expect(NOT_HIGHER_THAN_N_RESULT(50)).toBe("Le résultat ne peut pas être supérieur à 50");
expect(NOT_HIGHER_THAN_N_RESULT(100)).toBe("Le résultat ne peut pas être supérieur à 100");
});
});

describe("Reused messages", () => {
it("should use NOT_BELOW_N_RESULT for NOT_BELOW_0", () => {
expect(NOT_BELOW_0).toBe("Le résultat ne peut pas être inférieur à 0");
expect(NOT_BELOW_0).toBe(NOT_BELOW_N_RESULT(0));
});
});
});
Loading