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