diff --git a/frontend/jest.config.js b/frontend/jest.config.js new file mode 100644 index 0000000..a7a6340 --- /dev/null +++ b/frontend/jest.config.js @@ -0,0 +1,18 @@ +const nextJest = require('next/jest') + +const createJestConfig = nextJest({ + // Provide the path to your Next.js app to load next.config.js and .env files in your test environment + dir: './', +}) + +// Add any custom config to be passed to Jest +const customJestConfig = { + setupFilesAfterEnv: ['/jest.setup.js'], + testEnvironment: 'jest-environment-jsdom', + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, +} + +// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async +module.exports = createJestConfig(customJestConfig) diff --git a/frontend/jest.setup.js b/frontend/jest.setup.js new file mode 100644 index 0000000..29ae8eb --- /dev/null +++ b/frontend/jest.setup.js @@ -0,0 +1,4 @@ +import '@testing-library/jest-dom' + +// Mock fetch globally +global.fetch = jest.fn() diff --git a/frontend/package.json b/frontend/package.json index b486e93..2183fb5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "test": "jest" }, "dependencies": { "@hookform/resolvers": "^3.10.0", @@ -63,12 +64,18 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.1.9", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@types/jest": "^30.0.0", "@types/node": "^22", "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^9.39.1", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0", "postcss": "^8.5", "tailwindcss": "^4.1.9", + "ts-node": "^10.9.2", "tw-animate-css": "1.3.3", "typescript": "^5" } diff --git a/frontend/src/lib/__tests__/api.test.ts b/frontend/src/lib/__tests__/api.test.ts new file mode 100644 index 0000000..e0e3184 --- /dev/null +++ b/frontend/src/lib/__tests__/api.test.ts @@ -0,0 +1,107 @@ +let jobsApi: any +let companiesApi: any +let usersApi: any + +// Mock environment variable +const ORIGINAL_ENV = process.env + +beforeEach(() => { + jest.resetModules() + process.env = { ...process.env, NEXT_PUBLIC_API_URL: 'http://test-api.com/api/v1' } + + // Re-require modules to pick up new env vars + const api = require('../api') + jobsApi = api.jobsApi + companiesApi = api.companiesApi + usersApi = api.usersApi + + global.fetch = jest.fn() +}) + +afterEach(() => { + jest.clearAllMocks() +}) + +afterAll(() => { + process.env = ORIGINAL_ENV +}) + +describe('API Client', () => { + describe('jobsApi', () => { + it('should fetch jobs with correct parameters', async () => { + const mockJobs = { + data: [{ id: 1, title: 'Dev Job' }], + pagination: { total: 1 } + } + ; (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => mockJobs, + }) + + const response = await jobsApi.list({ page: 1, limit: 10, companyId: 5 }) + + expect(global.fetch).toHaveBeenCalledWith( + 'http://test-api.com/api/v1/jobs?page=1&limit=10&companyId=5', + expect.objectContaining({ + headers: expect.objectContaining({ + 'Content-Type': 'application/json' + }) + }) + ) + expect(response).toEqual(mockJobs) + }) + + it('should handle API errors correctly', async () => { + ; (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 404, + text: async () => 'Not Found', + }) + + await expect(jobsApi.list()).rejects.toThrow('Not Found') + }) + }) + + describe('companiesApi', () => { + it('should create company correctly', async () => { + const mockCompany = { id: '123', name: 'Test Corp' } + ; (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => mockCompany, + }) + + const newCompany = { name: 'Test Corp', slug: 'test-corp' } + await companiesApi.create(newCompany) + + expect(global.fetch).toHaveBeenCalledWith( + 'http://test-api.com/api/v1/companies', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(newCompany) + }) + ) + }) + }) + + describe('URL Construction', () => { + it('should handle double /api/v1 correctly', async () => { + ; (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => ([]), + }) + + // We need to import the raw apiRequest function to test this properly, + // but since it's not exported, we simulate via an exported function + await usersApi.list() + + // The apiRequest logic handles URL cleaning. + // Expected: base http://test-api.com/api/v1 + endpoint /api/v1/users -> http://test-api.com/api/v1/users + // NOT http://test-api.com/api/v1/api/v1/users + + expect(global.fetch).toHaveBeenCalledWith( + 'http://test-api.com/api/v1/users', + expect.any(Object) + ) + }) + }) +})