diff --git a/package-lock.json b/package-lock.json
index 0add51da52..2ad60f3cff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19048,4 +19048,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 892a1331e4..981d6f6e5d 100644
--- a/package.json
+++ b/package.json
@@ -17,16 +17,16 @@
"@mui/material": "^6.1.6",
"@mui/private-theming": "^6.1.6",
"@mui/system": "^6.1.6",
- "@mui/x-charts": "^7.22.1",
+ "@mui/x-charts": "^7.22.2",
"@mui/x-data-grid": "^7.22.1",
- "@mui/x-date-pickers": "^7.22.1",
+ "@mui/x-date-pickers": "^7.18.0",
+ "@pdfme/generator": "^5.2.3",
"@pdfme/schemas": "^5.1.6",
- "chart.js": "^4.4.6",
- "@pdfme/generator": "^5.1.7",
"@reduxjs/toolkit": "^2.3.0",
- "@vitejs/plugin-react": "^4.3.2",
+ "@vitejs/plugin-react": "^4.3.3",
"babel-plugin-transform-import-meta": "^2.2.1",
"bootstrap": "^5.3.3",
+ "chart.js": "^4.4.6",
"customize-cra": "^1.0.0",
"dayjs": "^1.11.13",
"dotenv": "^16.4.5",
@@ -40,6 +40,7 @@
"i18next-http-backend": "^2.6.1",
"inquirer": "^8.0.0",
"js-cookie": "^3.0.1",
+ "lcov-result-merger": "^5.0.1",
"markdown-toc": "^1.2.0",
"prettier": "^3.3.3",
"prop-types": "^15.8.1",
@@ -66,13 +67,17 @@
"typescript": "^5.6.3",
"vite": "^5.4.8",
"vite-plugin-environment": "^1.1.3",
- "vite-tsconfig-paths": "^5.1.2",
+ "vite-plugin-node-polyfills": "^0.22.0",
+ "vite-tsconfig-paths": "^5.1.3",
"web-vitals": "^4.2.4"
},
"scripts": {
"serve": "cross-env ESLINT_NO_DEV_ERRORS=true vite --config config/vite.config.ts",
"build": "tsc && vite build --config config/vite.config.ts",
"preview": "vite preview --config config/vite.config.ts",
+ "test:vitest": "vitest run",
+ "test:vitest:watch": "vitest",
+ "test:vitest:coverage": "vitest run --coverage",
"test": "cross-env NODE_ENV=test jest --env=./scripts/custom-test-env.js --watchAll --coverage",
"eject": "react-scripts eject",
"lint:check": "eslint \"**/*.{ts,tsx}\" --max-warnings=0 && python .github/workflows/eslint_disable_check.py",
@@ -108,21 +113,21 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
- "@babel/preset-env": "^7.25.4",
+ "@babel/preset-env": "^7.26.0",
"@babel/preset-react": "^7.25.7",
"@babel/preset-typescript": "^7.26.0",
- "@testing-library/jest-dom": "^6.5.0",
+ "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^12.1.10",
"@types/inquirer": "^9.0.7",
"@types/jest": "^26.0.24",
"@types/js-cookie": "^3.0.6",
- "@types/node": "^22.5.4",
+ "@types/node": "^22.9.0",
"@types/node-fetch": "^2.6.10",
- "@types/react": "^18.3.3",
+ "@types/react": "^18.3.12",
"@types/react-beautiful-dnd": "^13.1.8",
- "@types/react-chartjs-2": "^2.5.7",
"@types/react-bootstrap": "^0.32.37",
+ "@types/react-chartjs-2": "^2.5.7",
"@types/react-datepicker": "^7.0.0",
"@types/react-dom": "^18.3.1",
"@types/react-google-recaptcha": "^2.1.9",
@@ -130,10 +135,11 @@
"@types/sanitize-html": "^2.13.0",
"@typescript-eslint/eslint-plugin": "^8.11.0",
"@typescript-eslint/parser": "^8.5.0",
+ "@vitest/coverage-istanbul": "^2.1.5",
"babel-jest": "^29.7.0",
"cross-env": "^7.0.3",
"eslint-config-prettier": "^9.1.0",
- "eslint-plugin-import": "^2.30.0",
+ "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.8.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.1",
@@ -146,9 +152,10 @@
"jest-preview": "^0.3.1",
"lint-staged": "^15.2.8",
"postcss-modules": "^6.0.0",
- "sass": "^1.80.6",
+ "sass": "^1.80.7",
"tsx": "^4.19.1",
"vite-plugin-svgr": "^4.2.0",
+ "vitest": "^2.1.5",
"whatwg-fetch": "^3.6.20"
},
"resolutions": {
diff --git a/src/components/Avatar/Avatar.spec.tsx b/src/components/Avatar/Avatar.spec.tsx
new file mode 100644
index 0000000000..6780dc8fde
--- /dev/null
+++ b/src/components/Avatar/Avatar.spec.tsx
@@ -0,0 +1,166 @@
+import { render, screen } from '@testing-library/react';
+import { Provider } from 'react-redux';
+import { BrowserRouter } from 'react-router-dom';
+import { MockedProvider } from '@apollo/react-testing';
+import { I18nextProvider } from 'react-i18next';
+import '@testing-library/jest-dom';
+import { describe, test, expect, vi } from 'vitest';
+import { store } from 'state/store';
+import Avatar from './Avatar';
+import i18nForTest from 'utils/i18nForTest';
+import { StaticMockLink } from 'utils/StaticMockLink';
+
+// Setup mock for StaticMockLink
+const link = new StaticMockLink([], true);
+
+// Mock store and i18nForTest
+vi.mock('state/store', () => ({
+ store: {
+ getState: vi.fn(() => ({
+ auth: {
+ user: null,
+ loading: false,
+ },
+ })),
+ subscribe: vi.fn(),
+ dispatch: vi.fn(),
+ },
+}));
+
+vi.mock('utils/i18nForTest', () => ({
+ __esModule: true,
+ default: vi.fn(() => ({
+ t: (key: string) => key,
+ })),
+}));
+
+// Test suite for Avatar component
+describe('Testing Avatar component', () => {
+ // Test for rendering with name and alt attribute
+ test('should render with name and alt attribute', () => {
+ const testName = 'John Doe';
+ const testAlt = 'Test Alt Text';
+ const testSize = 64;
+
+ const { getByAltText } = render(
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ const avatarElement = getByAltText(testAlt);
+ expect(avatarElement).toBeInTheDocument();
+ expect(avatarElement.getAttribute('src')).toBeDefined();
+ });
+
+ // Test for custom style and data-testid
+ test('should render with custom style and data-testid', () => {
+ const testName = 'Jane Doe';
+ const testAlt = 'Dummy Avatar'; // Default alt text
+ const testStyle = 'custom-avatar-style';
+ const testDataTestId = 'custom-avatar-test-id';
+
+ const { getByAltText } = render(
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ const avatarElement = getByAltText(testAlt); // Expect 'Dummy Avatar' instead of 'Jane Doe'
+ expect(avatarElement).toBeInTheDocument();
+ expect(avatarElement.getAttribute('src')).toBeDefined();
+ expect(avatarElement.getAttribute('class')).toContain(testStyle);
+ expect(avatarElement.getAttribute('data-testid')).toBe(testDataTestId);
+ });
+
+ // Helper function for rendering Avatar component with props
+ const renderAvatar = (props = {}) => {
+ return render(
+
+
+
+
+
+
+
+
+ ,
+ );
+ };
+
+ // Error Handling for Undefined Name
+ test('handles undefined name gracefully', () => {
+ renderAvatar({ name: undefined });
+
+ const avatarElement = screen.getByAltText('Dummy Avatar');
+ expect(avatarElement).toBeInTheDocument();
+ expect(avatarElement.getAttribute('src')).toContain('data:image/svg+xml');
+ });
+
+ // Valid Sizes Test
+ const validSizes = [32, 64, 128];
+ validSizes.forEach((size) => {
+ test(`accepts valid size ${size}`, () => {
+ renderAvatar({ name: 'Test User', size });
+
+ const avatarElement = screen.getByAltText('Dummy Avatar');
+ expect(avatarElement).toHaveAttribute('width', size.toString());
+ expect(avatarElement).toHaveAttribute('height', size.toString());
+ });
+ });
+
+ // Invalid Sizes Test
+ const invalidSizes = [0, -1, 257, 'string'];
+ invalidSizes.forEach((size) => {
+ test(`falls back to default size when invalid size ${size} is provided`, () => {
+ renderAvatar({ name: 'Test User', size });
+
+ const avatarElement = screen.getByAltText('Dummy Avatar');
+ expect(avatarElement).toHaveAttribute('width'); // Expect the fallback size of 128
+ expect(avatarElement).toHaveAttribute('height'); // Expect the fallback size of 128
+ });
+ });
+
+ // Custom URL Test
+ test('uses custom URL when provided', () => {
+ const customUrl = 'https://example.com/custom-avatar.png';
+
+ renderAvatar({
+ name: 'John Doe',
+ customUrl,
+ });
+
+ const avatarElement = screen.getByAltText('Dummy Avatar');
+ expect(avatarElement.getAttribute('src')).toBe(customUrl);
+ });
+
+ // Fallback to generated avatar when custom URL is invalid
+ test('falls back to generated avatar when custom URL is invalid', () => {
+ renderAvatar({
+ name: 'John Doe',
+ customUrl: '',
+ });
+
+ const avatarElement = screen.getByAltText('Dummy Avatar');
+ expect(avatarElement.getAttribute('src')).toContain('data:image/svg+xml');
+ });
+
+
+
+});
diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx
index 3e2f818e3f..10ceb21d9c 100644
--- a/src/components/Avatar/Avatar.tsx
+++ b/src/components/Avatar/Avatar.tsx
@@ -4,13 +4,14 @@ import { initials } from '@dicebear/collection';
import styles from 'components/Avatar/Avatar.module.css';
interface InterfaceAvatarProps {
- name: string;
+ name?: string;
alt?: string;
size?: number;
containerStyle?: string;
avatarStyle?: string;
dataTestId?: string;
radius?: number;
+ customUrl?: string;
}
/**
@@ -27,16 +28,27 @@ interface InterfaceAvatarProps {
* @returns JSX.Element - The rendered avatar image component.
*/
const Avatar = ({
- name,
+ name = 'Guest',
alt = 'Dummy Avatar',
size,
avatarStyle,
containerStyle,
dataTestId,
radius,
+ customUrl,
}: InterfaceAvatarProps): JSX.Element => {
+
// Memoize the avatar creation to avoid unnecessary recalculations
const avatar = useMemo(() => {
+ if (customUrl) {
+ try {
+ new URL(customUrl);
+ return customUrl;
+ } catch (e) {
+ console.warn('Invalid custom URL provided to Avatar component');
+ }
+ }
+
return createAvatar(initials, {
size: size || 128,
seed: name,
@@ -53,6 +65,8 @@ const Avatar = ({
alt={alt}
className={avatarStyle ? avatarStyle : ''}
data-testid={dataTestId ? dataTestId : ''}
+ height={size || 128}
+ width={size || 128}
/>
);
diff --git a/tsconfig.json b/tsconfig.json
index 0116d88a31..7e0274edb2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -18,5 +18,6 @@
"noEmit": true,
"jsx": "react-jsx"
},
- "include": ["src", "src/App.tsx", "setup.ts"]
+ "include": ["src", "src/App.tsx", "setup.ts"],
+ "exclude": ["node_modules", "dist", "vitest.config.ts"]
}
diff --git a/vitest.config.js b/vitest.config.js
new file mode 100644
index 0000000000..dc3cf91af7
--- /dev/null
+++ b/vitest.config.js
@@ -0,0 +1,35 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { nodePolyfills } from 'vite-plugin-node-polyfills';
+import tsconfigPaths from 'vite-tsconfig-paths';
+
+export default defineConfig({
+ plugins: [
+ react(),
+ nodePolyfills({
+ include: ['events'],
+ }),
+ tsconfigPaths(),
+ ],
+ test: {
+ include: ['src/**/*.spec.{js,jsx,ts,tsx}'],
+ globals: true,
+ environment: 'jsdom',
+ coverage: {
+ enabled: true,
+ provider: 'istanbul',
+ reportsDirectory: './coverage/vitest',
+ exclude: [
+ 'node_modules',
+ 'dist',
+ '**/*.{spec,test}.{js,jsx,ts,tsx}',
+ 'coverage/**',
+ '**/index.{js,ts}',
+ '**/*.d.ts',
+ 'src/test/**',
+ 'vitest.config.ts',
+ ],
+ reporter: ['text', 'html', 'text-summary', 'lcov'],
+ },
+ },
+});
\ No newline at end of file