diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/App.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/App.test.tsx
index 936edc42..af2ae1d5 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/App.test.tsx
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/App.test.tsx
@@ -19,7 +19,7 @@ vi.mock('./services/analyticsService', () => ({
}));
// Mock the components
-vi.mock('./pages/login/Login', () => ({
+vi.mock('./pages/login', () => ({
default: () =>
Mocked Login Component
,
}));
vi.mock('./pages/logout/Logout', () => ({
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/App.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/App.tsx
index b0655bc5..acbb2834 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/App.tsx
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/App.tsx
@@ -1,4 +1,3 @@
-import Login from './pages/login/Login';
import {
ROUTE_LOGIN,
ROUTE_LOGIN_ERROR,
@@ -7,6 +6,7 @@ import {
import { redirectToLogin } from './utils/utils';
import Logout from './pages/logout/Logout';
import { LoginError } from './pages/loginError/LoginError';
+import Login from './pages/login';
const onLogout = () => ;
const onLoginError = () => ;
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.test.tsx
new file mode 100644
index 00000000..6325c2ed
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.test.tsx
@@ -0,0 +1,104 @@
+import { Mock, vi } from 'vitest';
+import { useLoginData } from './useLoginData';
+import { fetchBannerContent, getIdpList, getClientData } from '../services/api';
+import { ENV } from '../utils/env';
+import { renderHook, waitFor } from '@testing-library/react';
+
+vi.mock('../services/api', () => ({
+ fetchBannerContent: vi.fn(),
+ getIdpList: vi.fn(),
+ getClientData: vi.fn(),
+}));
+
+describe('useLoginData', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should fetch and set bannerContent, idpList, and clientData on mount', async () => {
+ const mockBanner = [
+ { enable: true, severity: 'info', description: 'Test Banner' },
+ ];
+ const mockIdpList = {
+ identityProviders: [{ entityID: 'test-idp' }],
+ richiediSpid: 'https://example.com/spid',
+ };
+ const mockClientData = {
+ clientID: 'test-client-id',
+ friendlyName: 'Test Client',
+ logoUri: 'https://example.com/logo.png',
+ policyUri: 'https://example.com/policy',
+ tosUri: 'https://example.com/tos',
+ };
+
+ // Mock API responses
+ (fetchBannerContent as Mock).mockResolvedValueOnce(mockBanner);
+ (getIdpList as Mock).mockResolvedValueOnce({ idps: mockIdpList });
+ (getClientData as Mock).mockResolvedValueOnce({
+ clientData: mockClientData,
+ });
+
+ // Render the hook
+ const { result } = renderHook(useLoginData);
+
+ await waitFor(() => {
+ expect(fetchBannerContent).toHaveBeenCalledWith(ENV.JSON_URL.ALERT);
+ expect(getIdpList).toHaveBeenCalledWith(ENV.JSON_URL.IDP_LIST);
+ expect(getClientData).toHaveBeenCalledWith(ENV.JSON_URL.CLIENT_BASE_URL);
+ });
+
+ // Check if the state is updated correctly
+ expect(result.current.bannerContent).toEqual(mockBanner);
+ expect(result.current.idpList).toEqual(mockIdpList);
+ expect(result.current.clientData).toEqual(mockClientData);
+ });
+
+ it('should not set state if API calls fail', async () => {
+ // Mock failed API responses
+ (fetchBannerContent as Mock).mockRejectedValueOnce(
+ new Error('Failed to fetch banner')
+ );
+ (getIdpList as Mock).mockRejectedValueOnce(
+ new Error('Failed to fetch IDP list')
+ );
+ (getClientData as Mock).mockRejectedValueOnce(
+ new Error('Failed to fetch client data')
+ );
+
+ const { result } = renderHook(useLoginData);
+
+ await waitFor(() => {
+ // Check that the state has not been set
+ expect(result.current.bannerContent).toBeUndefined();
+ expect(result.current.idpList).toEqual({
+ identityProviders: [],
+ richiediSpid: '',
+ });
+ expect(result.current.clientData).toBeUndefined();
+ });
+ });
+
+ it('should handle partial data successfully', async () => {
+ const mockBanner = [
+ { enable: true, severity: 'info', description: 'Test Banner' },
+ ];
+ const mockIdpList = {
+ identityProviders: [{ entityID: 'test-idp' }],
+ richiediSpid: 'https://example.com/spid',
+ };
+
+ // Mock API responses
+ (fetchBannerContent as Mock).mockResolvedValueOnce(mockBanner);
+ (getIdpList as Mock).mockResolvedValueOnce({ idps: mockIdpList });
+ (getClientData as Mock).mockResolvedValueOnce({ clientData: undefined }); // No client data
+
+ const { result } = renderHook(useLoginData);
+
+ await waitFor(() => {
+ // Verify that bannerContent and idpList are correctly set
+ expect(result.current.bannerContent).toEqual(mockBanner);
+ expect(result.current.idpList).toEqual(mockIdpList);
+ expect(result.current.clientData).toBeUndefined(); // clientData should be undefined
+ });
+ });
+});
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.tsx
new file mode 100644
index 00000000..b882efdb
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.tsx
@@ -0,0 +1,55 @@
+import { useEffect, useState } from 'react';
+import { ENV } from '../utils/env';
+import { IdentityProviders } from '../utils/IDPS';
+import {
+ type BannerContent,
+ type Client,
+ fetchBannerContent,
+ getIdpList,
+ getClientData,
+} from '../services/api';
+
+export const useLoginData = () => {
+ const [bannerContent, setBannerContent] = useState>();
+ const [idpList, setIdpList] = useState({
+ identityProviders: [],
+ richiediSpid: '',
+ });
+ const [clientData, setClientData] = useState();
+
+ useEffect(() => {
+ const bannerRequest = fetchBannerContent(ENV.JSON_URL.ALERT);
+ const idpsRequest = getIdpList(ENV.JSON_URL.IDP_LIST);
+ const clientDataRequest = getClientData(ENV.JSON_URL.CLIENT_BASE_URL);
+
+ Promise.allSettled([idpsRequest, clientDataRequest, bannerRequest]).then(
+ ([idpsResult, clientDataResult, bannerResult]) => {
+ if (idpsResult.status === 'fulfilled' && idpsResult.value.idps) {
+ setIdpList(idpsResult.value.idps);
+ } else {
+ console.error('Failed to fetch IDP list:', idpsResult.status);
+ }
+
+ if (
+ clientDataResult.status === 'fulfilled' &&
+ clientDataResult.value.clientData
+ ) {
+ setClientData(clientDataResult.value.clientData);
+ } else {
+ console.error(
+ 'Failed to fetch client data:',
+ clientDataResult.status
+ );
+ }
+
+ if (bannerResult.status === 'fulfilled' && bannerResult.value?.length) {
+ setBannerContent(bannerResult.value);
+ } else {
+ console.error('Failed to fetch banner content:', bannerResult.status);
+ }
+ }
+ );
+ }, []);
+
+ return { bannerContent, idpList, clientData };
+};
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/Login.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/Login.test.tsx
index d78f5434..55a97b31 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/Login.test.tsx
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/Login.test.tsx
@@ -1,20 +1,50 @@
/* eslint-disable functional/immutable-data */
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-import { afterAll, beforeAll, expect, Mock, test, vi } from 'vitest';
-
+import { afterAll, beforeAll, afterEach, test, vi, Mock } from 'vitest';
import { ENV } from '../../utils/env';
import { i18nTestSetup } from '../../__tests__/i18nTestSetup';
-import Login from './Login';
-
-// Mock fetch
-global.fetch = vi.fn();
-
+import Login from '.';
+
+// Constants for repeated strings
+const SPID_LOGIN = 'spidButton';
+const CIE_LOGIN = 'CIE Login';
+const LOGIN_TITLE = 'Login Title';
+const LOGIN_DESCRIPTION = 'Login Description';
+const TEMPORARY_LOGIN_ALERT = 'Temporary Login Alert';
+const ALERT_DESCRIPTION = 'This is a warning!';
+const MOCK_IDP_ENTITY_ID = 'test-idp';
+const MOCK_RICHIEDI_SPID_URL = 'https://example.com/spid';
+
+// Setup translations
i18nTestSetup({
loginPage: {
+ title: LOGIN_TITLE,
+ description: LOGIN_DESCRIPTION,
+ loginBox: {
+ spidLogin: SPID_LOGIN,
+ cieLogin: CIE_LOGIN,
+ },
privacyAndCondition: {
text: 'terms: {{termsLink}} privacy: {{privacyLink}}',
},
+ temporaryLogin: {
+ alert: TEMPORARY_LOGIN_ALERT,
+ join: 'Join',
+ },
},
+ spidSelect: {
+ modalTitle: 'test modal',
+ },
+});
+
+// Clear mocks after each test
+afterEach(() => {
+ vi.clearAllMocks();
+});
+
+beforeEach(() => {
+ // Mock fetch
+ global.fetch = vi.fn();
});
const oldWindowLocation = global.window.location;
@@ -28,22 +58,16 @@ afterAll(() => {
Object.defineProperty(window, 'location', { value: oldWindowLocation });
});
-// Clear mocks after each test
-afterEach(() => {
- vi.clearAllMocks();
-});
-
+// Test cases
test('Renders Login component', () => {
render();
- expect(screen.getByText('loginPage.title')).toBeInTheDocument();
+ expect(screen.getByText(LOGIN_TITLE)).toBeInTheDocument();
+ expect(screen.getByText(LOGIN_DESCRIPTION)).toBeInTheDocument();
});
-const mockWarning = 'This is a warning!';
-
test('Fetches and displays banner alerts', async () => {
- // Mock the fetch response
const mockBannerResponse = [
- { enable: true, severity: 'warning', description: mockWarning },
+ { enable: true, severity: 'warning', description: ALERT_DESCRIPTION },
];
(fetch as Mock).mockResolvedValueOnce({
json: vi.fn().mockResolvedValueOnce(mockBannerResponse),
@@ -52,7 +76,7 @@ test('Fetches and displays banner alerts', async () => {
render();
await waitFor(() => {
- expect(screen.getByText(mockWarning)).toBeInTheDocument();
+ expect(screen.getByText(ALERT_DESCRIPTION)).toBeInTheDocument();
});
});
@@ -61,16 +85,15 @@ test('Handles fetch error for alert message', async () => {
render();
- // Optionally check if an error message or warning is displayed
await waitFor(() => {
- expect(screen.queryByText(mockWarning)).not.toBeInTheDocument();
+ expect(screen.queryByText(ALERT_DESCRIPTION)).not.toBeInTheDocument();
});
});
test('Fetches IDP list on mount', async () => {
const mockIDPListResponse = {
- identityProviders: [{ entityID: 'test-idp' }],
- richiediSpid: 'https://example.com/spid',
+ identityProviders: [{ entityID: MOCK_IDP_ENTITY_ID }],
+ richiediSpid: MOCK_RICHIEDI_SPID_URL,
};
(fetch as Mock).mockResolvedValueOnce({
json: vi.fn().mockResolvedValueOnce(mockIDPListResponse),
@@ -84,7 +107,7 @@ test('Fetches IDP list on mount', async () => {
});
test('Handles invalid client ID gracefully', async () => {
- window.history.pushState({}, '', `?client_id=invalidId`);
+ window.history.pushState({}, '', '?client_id=invalidId');
render();
@@ -93,19 +116,9 @@ test('Handles invalid client ID gracefully', async () => {
});
});
-test('Clicking SPID button opens modal', () => {
- render();
- const buttonSpid = document.getElementById('spidButton');
- fireEvent.click(buttonSpid as HTMLElement);
-
- expect(screen.getByRole('dialog')).toBeInTheDocument(); // Check if modal opens
-});
-
test('Clicking CIE button redirects correctly', () => {
render();
- const buttonCIE = screen.getByRole('button', {
- name: 'loginPage.loginBox.cieLogin',
- });
+ const buttonCIE = screen.getByText(CIE_LOGIN);
fireEvent.click(buttonCIE);
expect(global.window.location.assign).toHaveBeenCalledWith(
@@ -113,26 +126,8 @@ test('Clicking CIE button redirects correctly', () => {
);
});
-test('Clicking terms and conditions link redirects correctly', () => {
- render();
-
- const termsConditionLink = screen.getByText(
- 'loginPage.privacyAndCondition.terms'
- );
- fireEvent.click(termsConditionLink);
-
- expect(global.window.location.assign).toHaveBeenCalledWith(
- ENV.URL_FOOTER.TERMS_AND_CONDITIONS
- );
-});
-
-test('Clicking privacy link redirects correctly', () => {
+test('Displays temporary login alert if enabled', () => {
render();
-
- const privacyLink = screen.getByText('loginPage.privacyAndCondition.privacy');
- fireEvent.click(privacyLink);
-
- expect(global.window.location.assign).toHaveBeenCalledWith(
- ENV.URL_FOOTER.PRIVACY_DISCLAIMER
- );
+ const temporaryLoginAlert = screen.getByText(TEMPORARY_LOGIN_ALERT);
+ expect(temporaryLoginAlert).toBeInTheDocument();
});
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/CieButton.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/CieButton.test.tsx
new file mode 100644
index 00000000..e7271b2a
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/CieButton.test.tsx
@@ -0,0 +1,36 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { describe, it, vi } from 'vitest';
+import { CieButton, CieButtonProps } from './CieButton';
+import { i18nTestSetup } from '../../../__tests__/i18nTestSetup';
+
+describe('CieButton', () => {
+ const BUTTON_TEXT = 'CIE Login';
+ const onClickMock = vi.fn();
+
+ const renderComponent = (props: Partial = {}) => {
+ i18nTestSetup({ 'loginPage.loginBox.cieLogin': BUTTON_TEXT });
+ render();
+ };
+
+ it('renders the button with the correct text', () => {
+ renderComponent();
+ expect(
+ screen.getByRole('button', { name: BUTTON_TEXT })
+ ).toBeInTheDocument();
+ });
+
+ it('displays the CIE icon', () => {
+ renderComponent();
+ const icon = screen.getByAltText('CIE Icon');
+ expect(icon).toBeInTheDocument();
+ expect(icon).toHaveAttribute('src', expect.stringContaining('CIEIcon'));
+ });
+
+ it('calls the onClick handler when clicked', async () => {
+ renderComponent();
+ const button = screen.getByRole('button', { name: BUTTON_TEXT });
+ await userEvent.click(button);
+ expect(onClickMock).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/CieButton.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/CieButton.tsx
new file mode 100644
index 00000000..a80dc189
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/CieButton.tsx
@@ -0,0 +1,33 @@
+import Icon from '@mui/material/Icon';
+import { useTranslation } from 'react-i18next';
+import Button from '@mui/material/Button';
+import CIEIcon from '../../../assets/CIEIcon.svg';
+
+export type CieButtonProps = {
+ onClick: () => void;
+};
+
+export const CieIconWrapper = () => (
+
+
+
+);
+
+export const CieButton = ({ onClick }: CieButtonProps) => {
+ const { t } = useTranslation();
+
+ return (
+ }
+ onClick={onClick}
+ >
+ {t('loginPage.loginBox.cieLogin')}
+
+ );
+};
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidButton.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidButton.test.tsx
new file mode 100644
index 00000000..c6a5e4fb
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidButton.test.tsx
@@ -0,0 +1,56 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { vi } from 'vitest';
+import { SpidButton, SpidButtonProps } from './SpidButton';
+import { i18nTestSetup } from '../../../__tests__/i18nTestSetup';
+
+const TEST_ID = 'spidButton';
+
+describe('SpidButton Component', () => {
+ const onClickMock = vi.fn();
+
+ beforeAll(() => {
+ i18nTestSetup({
+ 'loginPage.loginBox.spidLogin': 'Login with SPID',
+ });
+ });
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ const renderComponent = (props: SpidButtonProps) =>
+ render();
+
+ it('renders the button with the correct text', () => {
+ renderComponent({ loading: false, onClick: onClickMock });
+
+ const button = screen.getByTestId(TEST_ID);
+ expect(button).toBeInTheDocument();
+ expect(button).toHaveTextContent('Login with SPID');
+ });
+
+ it('renders the button in a loading state', () => {
+ renderComponent({ loading: true, onClick: onClickMock });
+
+ const button = screen.getByTestId(TEST_ID);
+ expect(button).toHaveAttribute('aria-busy', 'true');
+ expect(button).toBeDisabled();
+ });
+
+ it('triggers the onClick function when clicked', () => {
+ renderComponent({ loading: false, onClick: onClickMock });
+
+ const button = screen.getByTestId(TEST_ID);
+ fireEvent.click(button);
+
+ expect(onClickMock).toHaveBeenCalledOnce();
+ });
+
+ it('displays the SPID icon', () => {
+ renderComponent({ loading: false, onClick: onClickMock });
+
+ const icon = screen.getByAltText('SPID Icon');
+ expect(icon).toBeInTheDocument();
+ expect(icon).toHaveAttribute('src', expect.stringContaining('SpidIcon'));
+ });
+});
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidButton.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidButton.tsx
new file mode 100644
index 00000000..db0fd673
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidButton.tsx
@@ -0,0 +1,50 @@
+import LoadingButton from '@mui/lab/LoadingButton';
+import SpidIcon from '../../../assets/SpidIcon.svg';
+import Icon from '@mui/material/Icon';
+import Typography from '@mui/material/Typography';
+import { theme } from '@pagopa/mui-italia/dist/theme';
+import { useTranslation } from 'react-i18next';
+
+export const SpidIconWrapper = () => (
+
+
+
+);
+
+export type SpidButtonProps = {
+ loading: boolean;
+ onClick: () => void;
+};
+
+export const SpidButton = ({ loading, onClick }: SpidButtonProps) => {
+ const { t } = useTranslation();
+
+ return (
+ }
+ sx={{
+ borderRadius: '4px',
+ width: '100%',
+ marginBottom: '5px',
+ }}
+ variant="contained"
+ >
+
+ {t('loginPage.loginBox.spidLogin')}
+
+
+ );
+};
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidModal.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidModal.test.tsx
similarity index 87%
rename from src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidModal.test.tsx
rename to src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidModal.test.tsx
index 4e182917..603a59ed 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidModal.test.tsx
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidModal.test.tsx
@@ -1,12 +1,12 @@
import { render, screen, fireEvent } from '@testing-library/react';
-import { IdentityProviders } from '../../utils/IDPS';
-import { trackEvent } from '../../services/analyticsService';
-import { forwardSearchParams } from '../../utils/utils';
+import { IdentityProviders } from '../../../utils/IDPS';
+import { trackEvent } from '../../../services/analyticsService';
+import { forwardSearchParams } from '../../../utils/utils';
import SpidModal from './SpidModal';
-vi.mock('../../services/analyticsService');
-vi.mock('../../utils/utils', () => ({
+vi.mock('../../../services/analyticsService');
+vi.mock('../../../utils/utils', () => ({
forwardSearchParams: vi.fn(() => 'testParams'),
}));
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidModal.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidModal.tsx
similarity index 84%
rename from src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidModal.tsx
rename to src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidModal.tsx
index 11f1b747..862f0ee2 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidModal.tsx
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidModal.tsx
@@ -1,12 +1,12 @@
import { Button, Dialog, Grid, Icon, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
-import { IdentityProvider, IdentityProviders } from '../../utils/IDPS';
-import { trackEvent } from '../../services/analyticsService';
-import { forwardSearchParams } from '../../utils/utils';
-import { ENV } from '../../utils/env';
-import { ImageWithFallback } from '../../components/ImageFallback';
-import { IDP_PLACEHOLDER_IMG } from '../../utils/constants';
+import { IdentityProvider, IdentityProviders } from '../../../utils/IDPS';
+import { trackEvent } from '../../../services/analyticsService';
+import { forwardSearchParams } from '../../../utils/utils';
+import { ENV } from '../../../utils/env';
+import { ImageWithFallback } from '../../../components/ImageFallback';
+import { IDP_PLACEHOLDER_IMG } from '../../../utils/constants';
type Props = {
openSpidModal: boolean;
@@ -32,7 +32,11 @@ const SpidModal = ({ openSpidModal, setOpenSpidModal, idpList }: Props) => {
const { t } = useTranslation();
return (
-