diff --git a/v6y-apps/front/src/__tests__/app-details/VitalityGeneralInformationView-test.tsx b/v6y-apps/front/src/__tests__/app-details/VitalityGeneralInformationView-test.tsx index ee20d0ab..053b9c4a 100644 --- a/v6y-apps/front/src/__tests__/app-details/VitalityGeneralInformationView-test.tsx +++ b/v6y-apps/front/src/__tests__/app-details/VitalityGeneralInformationView-test.tsx @@ -1,10 +1,12 @@ import '@testing-library/jest-dom/vitest'; import { render, screen, waitFor } from '@testing-library/react'; -import { Mock, afterEach, describe, expect, it, vi } from 'vitest'; +import { useNavigationAdapter, useTranslationProvider } from '@v6y/ui-kit'; +import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import VitalityGeneralInformationView from '../../features/app-details/components/infos/VitalityGeneralInformationView'; import { useClientQuery } from '../../infrastructure/adapters/api/useQueryAdapter'; +// Mocks vi.mock('../../infrastructure/adapters/api/useQueryAdapter', () => ({ useClientQuery: vi.fn(), })); @@ -13,180 +15,211 @@ vi.mock('../../commons/utils/VitalityDataExportUtils', () => ({ exportAppDetailsDataToCSV: vi.fn(), })); +vi.mock('@v6y/ui-kit', async () => { + const originalModule = await vi.importActual('@v6y/ui-kit'); + return { + ...originalModule, + useNavigationAdapter: vi.fn(), + useTranslationProvider: vi.fn(), + InfoOutlined: () =>
, + DynamicLoader: (fn: () => any) => { + // This will mock the DynamicLoader to immediately load the component + // allowing us to mock VitalityAppInfos and VitalitySectionView + const Component = fn(); + return (props: any) => ; + }, + }; +}); + +// Mock specific components that are dynamically loaded or direct children +// We want to check the props passed to these, not their internal rendering. +vi.mock('../../commons/components/VitalitySectionView', () => ({ + default: vi.fn(({ isLoading, isEmpty, children }) => ( +
+ {isLoading &&
Loading...
} + {isEmpty &&
Empty
} + {!isLoading && !isEmpty && children} +
+ )), +})); + +vi.mock('../../commons/components/application-info/VitalityAppInfos', () => ({ + default: vi.fn(({ app }) =>
{app?.name}
), +})); + +const mockUseClientQuery = useClientQuery as Mock; +const mockGetUrlParams = vi.fn(); +const mockTranslate = vi.fn((key) => key); // Simple translation mock + describe('VitalityGeneralInformationView', () => { + beforeEach(() => { + (useNavigationAdapter as Mock).mockReturnValue({ + getUrlParams: mockGetUrlParams, + }); + (useTranslationProvider as Mock).mockReturnValue({ + translate: mockTranslate, + }); + mockUseClientQuery.mockImplementation(() => ({ + isLoading: false, + data: undefined, + refetch: vi.fn(), + })); + console.warn = vi.fn(); // Mock console.warn + }); + afterEach(() => { vi.clearAllMocks(); }); - it('shows loading state while fetching data', async () => { - (useClientQuery as Mock).mockReturnValue({ + it('shows loading state when useClientQuery indicates loading', () => { + mockGetUrlParams.mockReturnValue(['123']); // Valid ID + mockUseClientQuery.mockReturnValue({ isLoading: true, data: null, }); render(); - - expect(screen.getByTestId('mock-loader')).toBeInTheDocument(); + expect(screen.getByTestId('loading-indicator')).toBeInTheDocument(); + const sectionViewProps = (vi.mocked(require('../../commons/components/VitalitySectionView')).default as Mock).mock.calls[0][0]; + expect(sectionViewProps.isLoading).toBe(true); + expect(sectionViewProps.isEmpty).toBe(true); // Because appInfos is null/undefined initially }); - it('renders application details correctly when data is available', async () => { - (useClientQuery as Mock).mockReturnValue({ + it('renders application details correctly when _id is valid and data is available', async () => { + const mockAppInfos = { + _id: 1, + name: 'Vitality App', + acronym: 'VAP', + }; + mockGetUrlParams.mockReturnValue(['1']); + mockUseClientQuery.mockReturnValue({ isLoading: false, - data: { - getApplicationDetailsInfoByParams: { - _id: 1, - name: 'Vitality App', - acronym: 'VAP', - description: 'A powerful application for testing.', - contactMail: 'contact@vitality.com', - repo: { - organization: 'Vitality Org', - webUrl: 'https://github.com/vitality-org', - allBranches: ['main', 'develop'], - }, - links: [ - { label: 'Website', value: 'https://vitality.com' }, - { label: 'Documentation', value: 'https://docs.vitality.com' }, - ], - }, - }, + data: { getApplicationDetailsInfoByParams: mockAppInfos }, }); render(); await waitFor(() => { - expect(screen.getByText('Vitality App')).toBeInTheDocument(); - expect(screen.getByText('A powerful application for testing.')).toBeInTheDocument(); - expect(screen.getByText('vitality.appListPage.nbBranches2')).toBeInTheDocument(); - expect(screen.getByText('Vitality Org')).toBeInTheDocument(); - expect(screen.getByText('Website')).toBeInTheDocument(); - expect(screen.getByText('Documentation')).toBeInTheDocument(); + expect(screen.getByTestId('mock-vitality-app-infos')).toBeInTheDocument(); + expect(screen.getByText('Vitality App')).toBeInTheDocument(); // From mock VitalityAppInfos }); - }); - it('handles missing application data gracefully', async () => { - (useClientQuery as Mock).mockReturnValue({ - isLoading: false, - data: { getApplicationDetailsInfoByParams: {} }, // Empty data - }); + const sectionViewProps = (vi.mocked(require('../../commons/components/VitalitySectionView')).default as Mock).mock.calls[0][0]; + expect(sectionViewProps.isLoading).toBe(false); + expect(sectionViewProps.isEmpty).toBe(false); - render(); + const appInfosProps = (vi.mocked(require('../../commons/components/application-info/VitalityAppInfos')).default as Mock).mock.calls[0][0]; + expect(appInfosProps.app).toEqual(mockAppInfos); - await waitFor(() => { - expect(screen.getByTestId('empty-view')).toBeInTheDocument(); - }); + expect(mockUseClientQuery).toHaveBeenCalledWith(expect.objectContaining({ + queryCacheKey: ['getApplicationDetailsInfoByParams', '1'], + variables: { _id: 1 }, + enabled: true, + })); }); - it('handles API errors gracefully', async () => { - (useClientQuery as Mock).mockReturnValue({ + it('handles valid _id but API returns no appInfos (null)', async () => { + mockGetUrlParams.mockReturnValue(['2']); + mockUseClientQuery.mockReturnValue({ isLoading: false, - data: null, // Simulating an error + data: { getApplicationDetailsInfoByParams: null }, }); render(); await waitFor(() => { - expect(screen.getByTestId('empty-view')).toBeInTheDocument(); + expect(screen.getByTestId('empty-indicator')).toBeInTheDocument(); }); + const sectionViewProps = (vi.mocked(require('../../commons/components/VitalitySectionView')).default as Mock).mock.calls[0][0]; + expect(sectionViewProps.isLoading).toBe(false); + expect(sectionViewProps.isEmpty).toBe(true); + expect(screen.queryByTestId('mock-vitality-app-infos')).not.toBeInTheDocument(); + expect(mockUseClientQuery).toHaveBeenCalledWith(expect.objectContaining({ + queryCacheKey: ['getApplicationDetailsInfoByParams', '2'], + variables: { _id: 2 }, + enabled: true, + })); }); - it('renders application without optional fields gracefully', async () => { - (useClientQuery as Mock).mockReturnValue({ + it('handles valid _id but API returns no appInfos (undefined data)', async () => { + mockGetUrlParams.mockReturnValue(['3']); + mockUseClientQuery.mockReturnValue({ isLoading: false, - data: { - getApplicationDetailsInfoByParams: { - _id: 2, - name: 'Minimal App', - acronym: 'MAP', - // No description, contactMail, repo, or links - }, - }, + data: undefined, // Simulate data being undefined }); render(); await waitFor(() => { - expect(screen.getByText('Minimal App')).toBeInTheDocument(); + expect(screen.getByTestId('empty-indicator')).toBeInTheDocument(); }); - - expect(screen.queryByText('A powerful application for testing.')).not.toBeInTheDocument(); - expect(screen.queryByText('Website')).not.toBeInTheDocument(); - expect(screen.queryByText('Vitality Org')).not.toBeInTheDocument(); + const sectionViewProps = (vi.mocked(require('../../commons/components/VitalitySectionView')).default as Mock).mock.calls[0][0]; + expect(sectionViewProps.isLoading).toBe(false); + expect(sectionViewProps.isEmpty).toBe(true); + expect(mockUseClientQuery).toHaveBeenCalledWith(expect.objectContaining({ + queryCacheKey: ['getApplicationDetailsInfoByParams', '3'], + variables: { _id: 3 }, + enabled: true, + })); }); - it('does not render application with missing required fields', async () => { - (useClientQuery as Mock).mockReturnValue({ - isLoading: false, - data: { - getApplicationDetailsInfoByParams: { - _id: 3, - acronym: '', // Missing name and acronym - description: 'This should not be displayed', - }, - }, - }); + + it('handles invalid _id string (e.g., "abc"), disables query, and shows empty state', async () => { + mockGetUrlParams.mockReturnValue(['abc']); // Invalid _id + // useClientQuery will use its default mock return (isLoading: false, data: undefined) + // because enabled should be false render(); await waitFor(() => { - expect(screen.getByTestId('empty-view')).toBeInTheDocument(); + // VitalitySectionView should be in empty state because appInfos is undefined + expect(screen.getByTestId('empty-indicator')).toBeInTheDocument(); }); - expect(screen.queryByText('This should not be displayed')).not.toBeInTheDocument(); + expect(mockUseClientQuery).toHaveBeenCalledWith(expect.objectContaining({ + queryCacheKey: ['getApplicationDetailsInfoByParams', undefined], // numericId will be undefined + variables: { _id: undefined }, + enabled: false, // Query should be disabled + })); + const sectionViewProps = (vi.mocked(require('../../commons/components/VitalitySectionView')).default as Mock).mock.calls[0][0]; + expect(sectionViewProps.isLoading).toBe(false); // isLoading is false from default mock + expect(sectionViewProps.isEmpty).toBe(true); // isEmpty because appInfos is undefined + expect(console.warn).toHaveBeenCalledWith('Invalid or missing _id parameter:', 'abc'); + expect(screen.queryByTestId('mock-vitality-app-infos')).not.toBeInTheDocument(); }); - it('renders repository and links correctly when present', async () => { - (useClientQuery as Mock).mockReturnValue({ - isLoading: false, - data: { - getApplicationDetailsInfoByParams: { - _id: 4, - name: 'Vitality Repo Test', - acronym: 'VRT', - repo: { - organization: 'Vitality Org', - webUrl: 'https://github.com/vitality-org', - allBranches: ['main', 'feature-x'], - }, - links: [{ label: 'Docs', value: 'https://docs.vitality.com' }], - }, - }, - }); + it('handles undefined _id, disables query, and shows empty state', async () => { + mockGetUrlParams.mockReturnValue([undefined]); // _id is undefined + // useClientQuery will use its default mock return (isLoading: false, data: undefined) render(); await waitFor(() => { - expect(screen.getByText('Vitality Repo Test')).toBeInTheDocument(); - expect(screen.getByText('Vitality Org')).toBeInTheDocument(); - expect(screen.getByText('Docs')).toBeInTheDocument(); + expect(screen.getByTestId('empty-indicator')).toBeInTheDocument(); }); + + expect(mockUseClientQuery).toHaveBeenCalledWith(expect.objectContaining({ + queryCacheKey: ['getApplicationDetailsInfoByParams', undefined], + variables: { _id: undefined }, + enabled: false, + })); + const sectionViewProps = (vi.mocked(require('../../commons/components/VitalitySectionView')).default as Mock).mock.calls[0][0]; + expect(sectionViewProps.isLoading).toBe(false); + expect(sectionViewProps.isEmpty).toBe(true); + expect(console.warn).toHaveBeenCalledWith('Invalid or missing _id parameter:', undefined); + expect(screen.queryByTestId('mock-vitality-app-infos')).not.toBeInTheDocument(); }); - it('does not render empty repository fields', async () => { - (useClientQuery as Mock).mockReturnValue({ - isLoading: false, - data: { - getApplicationDetailsInfoByParams: { - _id: 5, - name: 'Vitality No Repo', - acronym: 'VNR', - repo: { - organization: '', - webUrl: '', - allBranches: [], - }, - links: [], - }, - }, - }); + it('correctly forms queryCacheKey with valid numeric string _id', () => { + mockGetUrlParams.mockReturnValue(['456']); + mockUseClientQuery.mockReturnValue({ isLoading: false, data: null }); render(); - await waitFor(() => { - expect(screen.getByText('Vitality No Repo')).toBeInTheDocument(); - }); - - expect(screen.queryByText('Vitality Org')).not.toBeInTheDocument(); - expect(screen.queryByText('Docs')).not.toBeInTheDocument(); + expect(mockUseClientQuery).toHaveBeenCalledWith(expect.objectContaining({ + queryCacheKey: ['getApplicationDetailsInfoByParams', '456'], + variables: { _id: 456 }, + enabled: true, + })); }); }); diff --git a/v6y-apps/front/src/features/app-details/components/infos/VitalityGeneralInformationView.tsx b/v6y-apps/front/src/features/app-details/components/infos/VitalityGeneralInformationView.tsx index a0c4935b..89635b99 100644 --- a/v6y-apps/front/src/features/app-details/components/infos/VitalityGeneralInformationView.tsx +++ b/v6y-apps/front/src/features/app-details/components/infos/VitalityGeneralInformationView.tsx @@ -27,21 +27,29 @@ interface VitalityGeneralInformationQueryType { const VitalityGeneralInformationView = ({}) => { const { getUrlParams } = useNavigationAdapter(); - const [_id] = getUrlParams(['_id']); + const [rawId] = getUrlParams(['_id']); + let numericId: number | undefined; + + if (typeof rawId === 'string' && /^\d+$/.test(rawId)) { + numericId = parseInt(rawId, 10); + } else { + console.warn('Invalid or missing _id parameter:', rawId); + } const { isLoading: isAppDetailsInfosLoading, data: appDetailsInfos, }: VitalityGeneralInformationQueryType = useClientQuery({ - queryCacheKey: ['getApplicationDetailsInfoByParams', `${_id}`], + queryCacheKey: ['getApplicationDetailsInfoByParams', numericId ? String(numericId) : undefined], queryBuilder: async () => buildClientQuery({ queryBaseUrl: VitalityApiConfig.VITALITY_BFF_URL, query: GetApplicationDetailsInfosByParams, variables: { - _id: parseInt(_id as string, 10), + _id: numericId, }, }), + enabled: numericId !== undefined, }); const appInfos = appDetailsInfos?.getApplicationDetailsInfoByParams; @@ -54,7 +62,7 @@ const VitalityGeneralInformationView = ({}) => { return ( }