Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
32 changes: 32 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
},
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/main.tsx',
'!src/index.tsx',
'!src/mocks/**',
'!src/types.ts',
'!src/**/*.d.ts',
'!src/**/index.ts', // ignore all index.ts files
'!src/config/**', // ignore config files
'!src/layout/**', // ignore layout files
'!src/App.tsx', // ignore root App
'!src/ErrorBoundary.tsx', // ignore error boundary
'!src/Root.tsx' // ignore root entry
],
coverageThreshold: {
global: {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
}
};
1 change: 1 addition & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('@testing-library/jest-dom');
16 changes: 14 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"scripts": {
"ci": "yarn install --frozen-lockfile",
"start": "webpack serve --config webpack.dev.js --open",
"build": "webpack --config webpack.prod.js"
"build": "webpack --config webpack.prod.js",
"test": "jest",
"test:coverage": "jest --coverage"
},
"keywords": [
"react",
Expand All @@ -29,10 +31,20 @@
"react-router": "^7.3.0"
},
"devDependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^29.5.14",
"@types/mocha": "^10.0.10",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"html-webpack-plugin": "^5.6.3",
"identity-obj-proxy": "^3.0.0",
"jest": "^30.0.0",
"jest-environment-jsdom": "^30.0.0",
"msw": "^2.8.6",
"ts-jest": "^29.4.0",
"ts-loader": "^9.5.2",
"typescript": "^5.8.2",
"webpack": "^5.98.0",
Expand All @@ -48,4 +60,4 @@
"public"
]
}
}
}
14 changes: 14 additions & 0 deletions src/components/Footer/Footer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { render, screen } from '@testing-library/react';
import Footer from './Footer';

describe('Footer', () => {
it('renders the footer with copyright and link', () => {
render(<Footer />);
const year = new Date().getFullYear();
expect(screen.getByText(new RegExp(`${year}`))).toBeInTheDocument();
expect(screen.getByText(/Powered By/i)).toBeInTheDocument();
const link = screen.getByRole('link', { name: /webtechpie.com/i });
expect(link).toHaveAttribute('href', 'https://webtechpie.com/');
expect(link).toHaveAttribute('target', '_blank');
});
});
13 changes: 13 additions & 0 deletions src/components/HelloWorld.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { render, screen } from '@testing-library/react';
import React from 'react';

function HelloWorld() {
return <div>Hello, World!</div>;
}

describe('HelloWorld', () => {
it('renders greeting', () => {
render(<HelloWorld />);
expect(screen.getByText(/hello, world/i)).toBeInTheDocument();
});
});
36 changes: 36 additions & 0 deletions src/components/Navigation/Navigation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { render, screen, fireEvent } from '@testing-library/react';
import Navigation from './Navigation';
import * as ReactRouter from 'react-router';

// Mock router config
jest.mock('../../config/router.config', () => ({
MAIN_ROUTES: [
{ path: '/', title: 'Home', name: 'home' },
{ path: '/about', title: 'About', name: 'about' }
],
DASHBOARD_NESTED_ROUTES: [
{ path: '/dashboard', title: 'Dashboard', name: 'dashboard' }
]
}));

// Mock Link to render an anchor
jest.mock('react-router', () => ({
Link: ({ to, children }: any) => <a href={to}>{children}</a>
}));

describe('Navigation', () => {
it('renders navigation links', () => {
render(<Navigation />);
expect(screen.getAllByText('Home').length).toBeGreaterThan(0);
expect(screen.getAllByText('About').length).toBeGreaterThan(0);
expect(screen.getAllByText('Dashboard').length).toBeGreaterThan(0);
});

it('toggles drawer on icon click (mobile)', () => {
render(<Navigation />);
const button = screen.getByLabelText(/open drawer/i);
fireEvent.click(button);
// After click, the drawer should be rendered (RTW text is in drawer)
expect(screen.getAllByText('RTW')[0]).toBeInTheDocument();
});
});
14 changes: 14 additions & 0 deletions src/components/StandardImageList/StandardImageList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { render, screen } from '@testing-library/react';
import StandardImageList from './StandardImageList';

describe('StandardImageList', () => {
it('renders a list of images with alt text', () => {
render(<StandardImageList />);
// There are 12 items in itemData
const images = screen.getAllByRole('img');
expect(images.length).toBeGreaterThanOrEqual(12);
// Check for a specific alt text from the data
expect(screen.getByAltText('Breakfast')).toBeInTheDocument();
expect(screen.getByAltText('Bike')).toBeInTheDocument();
});
});
45 changes: 45 additions & 0 deletions src/services/app.services.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { fetchUser, createTodo } from './app.services';
import axios from 'axios';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('app.services', () => {
describe('fetchUser', () => {
it('returns user data on success', async () => {
mockedAxios.get.mockResolvedValueOnce({ data: { name: 'Test User' } });
const data = await fetchUser();
expect(data).toEqual({ name: 'Test User' });
expect(mockedAxios.get).toHaveBeenCalledWith('https://raw.githubusercontent.com/hidaytrahman/hidaytrahman/main/me.json');
});

it('logs error and returns undefined on failure', async () => {
const error = new Error('Network error');
mockedAxios.get.mockRejectedValueOnce(error);
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
const data = await fetchUser();
expect(data).toBeUndefined();
expect(spy).toHaveBeenCalledWith(error);
spy.mockRestore();
});
});

describe('createTodo', () => {
it('returns data on success', async () => {
mockedAxios.post.mockResolvedValueOnce({ data: { id: 1, title: 'Test', completed: false } });
const result = await createTodo('Test', false);
expect(result).toEqual({ data: { id: 1, title: 'Test', completed: false }, error: null });
expect(mockedAxios.post).toHaveBeenCalledWith('https://jsonplaceholder.typicode.com/todos', { title: 'Test', completed: false });
});

it('returns error on failure', async () => {
const error = new Error('Post error');
mockedAxios.post.mockRejectedValueOnce(error);
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
const result = await createTodo('Test', false);
expect(result).toEqual({ data: null, error: 'Post error' });
expect(spy).toHaveBeenCalledWith(error);
spy.mockRestore();
});
});
});
8 changes: 8 additions & 0 deletions src/utils/theme.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { theme } from './theme.utils';

describe('theme.utils', () => {
it('should have primary and secondary colors', () => {
expect(theme.palette.primary.main).toBe('#8A784E');
expect(theme.palette.secondary.main).toBe('#edf2ff');
});
});
13 changes: 13 additions & 0 deletions src/views/About/About.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { render, screen } from '@testing-library/react';
import About from './About';

describe('About', () => {
it('renders About Us heading and team members', () => {
render(<About />);
expect(screen.getByRole('heading', { name: /about us/i })).toBeInTheDocument();
expect(screen.getByText(/welcome to our company/i)).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /meet our team/i })).toBeInTheDocument();
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});
});
11 changes: 11 additions & 0 deletions src/views/Contact/Contact.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { render, screen } from '@testing-library/react';
import Contact from './Contact';

describe('Contact', () => {
it('renders Contact Us heading and mission text', () => {
render(<Contact />);
expect(screen.getByRole('heading', { name: /contact us/i })).toBeInTheDocument();
expect(screen.getByText(/welcome to our company/i)).toBeInTheDocument();
expect(screen.getByText(/continuous improvement/i)).toBeInTheDocument();
});
});
9 changes: 9 additions & 0 deletions src/views/Dashboard/Home/Home.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { render, screen } from '@testing-library/react';
import Home from './Home';

describe('Home', () => {
it('renders Home heading', () => {
render(<Home />);
expect(screen.getByRole('heading', { name: /home/i })).toBeInTheDocument();
});
});
24 changes: 24 additions & 0 deletions src/views/Dashboard/Profile/Profile.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { render, screen, waitFor } from '@testing-library/react';
import Profile from './Profile';

jest.mock('@/services/app.services', () => ({
fetchUser: jest.fn().mockResolvedValue({
name: 'Test User',
greet: 'Hello!',
location: 'Test City',
intro: 'Test intro',
totalExperience: 5
})
}));

describe('Profile', () => {
it('renders loading and then user profile', async () => {
render(<Profile />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByRole('img', { name: 'Test User' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Hello!' })).toBeInTheDocument();
expect(screen.getByText('Test intro')).toBeInTheDocument();
});
});
});
9 changes: 9 additions & 0 deletions src/views/Dashboard/Settings/Settings.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { render, screen } from '@testing-library/react';
import Settings from './Settings';

describe('Settings', () => {
it('renders Settings heading', () => {
render(<Settings />);
expect(screen.getByRole('heading', { name: /settings/i })).toBeInTheDocument();
});
});
12 changes: 12 additions & 0 deletions src/views/Landing/Landing.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { render, screen } from '@testing-library/react';
import Landing from './Landing';

describe('Landing', () => {
it('renders landing page with StandardImageList', () => {
render(<Landing />);
expect(screen.getByRole('heading', { name: /hello, react with typescript and webpack/i })).toBeInTheDocument();
expect(screen.getByText(/simple react application/i)).toBeInTheDocument();
// StandardImageList renders at least one image
expect(screen.getAllByRole('img').length).toBeGreaterThan(0);
});
});
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
// Maps aliases to specific directories.
"@/*": ["./src/*"],
"components/*": ["./src/components/*"]
}
},
"types": ["@testing-library/jest-dom"]
},
"include": ["src"]
}
Loading
Loading