diff --git a/packages/apps/esm-devtools-app/src/devtools/devtools.component.test.tsx b/packages/apps/esm-devtools-app/src/devtools/devtools.component.test.tsx
index c3349a1c8..4317d9673 100644
--- a/packages/apps/esm-devtools-app/src/devtools/devtools.component.test.tsx
+++ b/packages/apps/esm-devtools-app/src/devtools/devtools.component.test.tsx
@@ -1,9 +1,75 @@
import React from 'react';
+import '@testing-library/jest-dom';
+import userEvent from '@testing-library/user-event';
+import { type AppProps } from 'single-spa';
+import { render, screen } from '@testing-library/react';
import Root from './devtools.component';
-import { render } from '@testing-library/react';
-describe(``, () => {
- it(`renders without dying`, () => {
- render();
+jest.mock('./import-map.component', () => ({
+ __esModule: true,
+ default: () =>
Mock Import Map
,
+ importMapOverridden: false,
+}));
+
+const defaultProps: AppProps = {
+ name: '@openmrs/esm-devtools-app-page-0',
+ singleSpa: {},
+ mountParcel: jest.fn(),
+};
+
+describe('DevTools', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ delete window.spaEnv;
+ jest.resetModules();
+ });
+
+ describe('Root component', () => {
+ it('should not render DevTools in production without the devtools localStorage flag', () => {
+ window.spaEnv = 'production';
+
+ const { container } = render();
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('should render DevTools in development environments', () => {
+ window.spaEnv = 'development';
+
+ render();
+
+ expect(screen.getByRole('button', { name: '{···}' })).toBeInTheDocument();
+ });
+
+ it('should render DevTools when the devtools localStorage flag is set', () => {
+ localStorage.setItem('openmrs:devtools', 'true');
+
+ render();
+
+ expect(screen.getByRole('button', { name: '{···}' })).toBeInTheDocument();
+ });
+ });
+
+ describe('DevTools component', () => {
+ const user = userEvent.setup();
+
+ beforeEach(() => {
+ window.spaEnv = 'development';
+ });
+
+ it('should toggle DevToolsPopup when clicking trigger button', async () => {
+ render();
+
+ const triggerButton = screen.getByRole('button', { name: '{···}' });
+ // Initially, popup should not be present
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+
+ // Click to open
+ await user.click(triggerButton);
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+
+ // Click to close
+ await user.click(triggerButton);
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
});
});
diff --git a/packages/apps/esm-devtools-app/src/devtools/devtools.component.tsx b/packages/apps/esm-devtools-app/src/devtools/devtools.component.tsx
index aff566e55..a2b0f96d3 100644
--- a/packages/apps/esm-devtools-app/src/devtools/devtools.component.tsx
+++ b/packages/apps/esm-devtools-app/src/devtools/devtools.component.tsx
@@ -1,18 +1,20 @@
import React, { useState } from 'react';
import classNames from 'classnames';
+import { type AppProps } from 'single-spa';
import { importMapOverridden } from './import-map.component';
import DevToolsPopup from './devtools-popup.component';
import styles from './devtools.styles.css';
-export default function Root(props) {
+export default function Root(props: AppProps) {
return window.spaEnv === 'development' || Boolean(localStorage.getItem('openmrs:devtools')) ? (
) : null;
}
-function DevTools() {
+function DevTools(props: AppProps) {
const [devToolsOpen, setDevToolsOpen] = useState(false);
const [isOverridden, setIsOverridden] = useState(importMapOverridden);
+
return (
<>
-
-
- );
+ return {}
;
}
export function importMapOverridden(): boolean {
diff --git a/packages/apps/esm-help-menu-app/src/root.component.test.tsx b/packages/apps/esm-help-menu-app/src/root.component.test.tsx
index 53b9b423a..867dbd996 100644
--- a/packages/apps/esm-help-menu-app/src/root.component.test.tsx
+++ b/packages/apps/esm-help-menu-app/src/root.component.test.tsx
@@ -2,8 +2,8 @@ import React from 'react';
import { render } from '@testing-library/react';
import Root from './root.component';
-describe(``, () => {
- it(`renders without dying`, () => {
+describe('Root', () => {
+ it('renders without dying', () => {
render();
});
});
diff --git a/packages/apps/esm-help-menu-app/src/root.component.tsx b/packages/apps/esm-help-menu-app/src/root.component.tsx
index 1d3f7dbf5..d074a7dca 100644
--- a/packages/apps/esm-help-menu-app/src/root.component.tsx
+++ b/packages/apps/esm-help-menu-app/src/root.component.tsx
@@ -4,4 +4,5 @@ import HelpMenu from './help-menu/help.component';
const Root: React.FC = () => {
return ;
};
+
export default Root;
diff --git a/packages/apps/esm-implementer-tools-app/src/configuration/configuration.test.tsx b/packages/apps/esm-implementer-tools-app/src/configuration/configuration.test.tsx
index 97ccaf9ed..f5e4bf179 100644
--- a/packages/apps/esm-implementer-tools-app/src/configuration/configuration.test.tsx
+++ b/packages/apps/esm-implementer-tools-app/src/configuration/configuration.test.tsx
@@ -6,8 +6,8 @@ import { implementerToolsConfigStore, temporaryConfigStore, Type } from '@openmr
import { Configuration } from './configuration.component';
import { useConceptLookup, useGetConceptByUuid } from './interactive-editor/value-editors/concept-search.resource';
-const mockUseConceptLookup = useConceptLookup as jest.Mock;
-const mockUseGetConceptByUuid = useGetConceptByUuid as jest.Mock;
+const mockUseConceptLookup = jest.mocked(useConceptLookup);
+const mockUseGetConceptByUuid = jest.mocked(useGetConceptByUuid);
jest.mock('./interactive-editor/value-editors/concept-search.resource', () => ({
useConceptLookup: jest.fn().mockImplementation(() => ({
@@ -144,14 +144,20 @@ describe('Configuration', () => {
const user = userEvent.setup();
mockUseConceptLookup.mockImplementation(() => ({
- concepts: [{ uuid: '61523693-72e2-456d-8c64-8c5293febeb6', display: 'Fedora' }],
- error: null,
+ concepts: [{ uuid: '61523693-72e2-456d-8c64-8c5293febeb6', display: 'Fedora', answers: [], mappings: [] }],
+ error: undefined,
isSearchingConcepts: false,
}));
mockUseGetConceptByUuid.mockImplementation(() => ({
- concept: { name: { display: 'Fedora' } },
- error: null,
+ concept: {
+ name: { display: 'Fedora' },
+ display: 'Fedora',
+ answers: [],
+ mappings: [],
+ uuid: '61523693-72e2-456d-8c64-8c5293febeb6',
+ },
+ error: undefined,
isLoadingConcept: false,
}));
diff --git a/packages/apps/esm-implementer-tools-app/src/implementer-tools.test.tsx b/packages/apps/esm-implementer-tools-app/src/implementer-tools.test.tsx
index 457892c96..b285e8290 100644
--- a/packages/apps/esm-implementer-tools-app/src/implementer-tools.test.tsx
+++ b/packages/apps/esm-implementer-tools-app/src/implementer-tools.test.tsx
@@ -2,8 +2,8 @@ import React from 'react';
import { render } from '@testing-library/react';
import Root from './implementer-tools.component';
-describe(``, () => {
- it(`renders without dying`, () => {
+describe('ImplementerTools', () => {
+ it('renders without dying', () => {
render();
});
});
diff --git a/packages/apps/esm-login-app/src/change-location-link/change-location-link.test.tsx b/packages/apps/esm-login-app/src/change-location-link/change-location-link.test.tsx
index 737f93bbe..3dcfb4672 100644
--- a/packages/apps/esm-login-app/src/change-location-link/change-location-link.test.tsx
+++ b/packages/apps/esm-login-app/src/change-location-link/change-location-link.test.tsx
@@ -1,22 +1,22 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
-import { navigate, useSession } from '@openmrs/esm-framework';
+import { navigate, type Session, useSession } from '@openmrs/esm-framework';
import ChangeLocationLink from './change-location-link.extension';
-const navigateMock = navigate as jest.Mock;
-const useSessionMock = useSession as jest.Mock;
+const mockNavigate = jest.mocked(navigate);
+const mockUseSession = jest.mocked(useSession);
delete window.location;
window.location = new URL('https://dev3.openmrs.org/openmrs/spa/home') as unknown as Location;
-describe('', () => {
+describe('ChangeLocationLink', () => {
beforeEach(() => {
- useSessionMock.mockReturnValue({
+ mockUseSession.mockReturnValue({
sessionLocation: {
display: 'Waffle House',
},
- });
+ } as Session);
});
it('should display the `Change location` link', async () => {
@@ -29,7 +29,7 @@ describe('', () => {
await user.click(changeLocationButton);
- expect(navigateMock).toHaveBeenCalledWith({
+ expect(mockNavigate).toHaveBeenCalledWith({
to: '${openmrsSpaBase}/login/location?returnToUrl=/openmrs/spa/home&update=true',
});
});
diff --git a/packages/apps/esm-login-app/src/change-password/change-password-link.test.tsx b/packages/apps/esm-login-app/src/change-password/change-password-link.test.tsx
index 123d27955..bd397af8c 100644
--- a/packages/apps/esm-login-app/src/change-password/change-password-link.test.tsx
+++ b/packages/apps/esm-login-app/src/change-password/change-password-link.test.tsx
@@ -6,8 +6,8 @@ import ChangePasswordLink from './change-password-link.extension';
const mockShowModal = jest.mocked(showModal);
-describe('', () => {
- it('should display the `Change password` link', async () => {
+describe('ChangePasswordLink', () => {
+ it('should launch the change password modal', async () => {
const user = userEvent.setup();
render();
diff --git a/packages/apps/esm-login-app/src/config-schema.ts b/packages/apps/esm-login-app/src/config-schema.ts
index cfe544a30..28022d667 100644
--- a/packages/apps/esm-login-app/src/config-schema.ts
+++ b/packages/apps/esm-login-app/src/config-schema.ts
@@ -78,18 +78,18 @@ export const configSchema = {
_type: Type.String,
_required: true,
_description: 'The source URL of the logo image',
- _validations: [validators.isUrl]
+ _validations: [validators.isUrl],
},
alt: {
_type: Type.String,
_required: true,
- _description: 'The alternative text for the logo image'
- }
- }
+ _description: 'The alternative text for the logo image',
+ },
+ },
},
_default: [],
_description: 'An array of logos to be displayed in the footer next to the OpenMRS logo.',
- }
+ },
},
showPasswordOnSeparateScreen: {
_type: Type.Boolean,
@@ -100,17 +100,18 @@ export const configSchema = {
};
export interface ConfigSchema {
- provider: {
- loginUrl: string;
- logoutUrl: string;
- type: string;
- };
chooseLocation: {
enabled: boolean;
locationsPerRequest: number;
numberToShow: number;
useLoginLocationTag: boolean;
};
+ footer: {
+ additionalLogos: Array<{
+ alt: string;
+ src: string;
+ }>;
+ };
links: {
loginSuccess: string;
};
@@ -118,11 +119,10 @@ export interface ConfigSchema {
alt: string;
src: string;
};
- footer: {
- additionalLogos: Array<{
- src: string;
- alt: string;
- }>;
+ provider: {
+ loginUrl: string;
+ logoutUrl: string;
+ type: string;
};
showPasswordOnSeparateScreen: boolean;
}
diff --git a/packages/apps/esm-login-app/src/location-picker/location-picker.test.tsx b/packages/apps/esm-login-app/src/location-picker/location-picker.test.tsx
index 2aac877dc..63d12e04e 100644
--- a/packages/apps/esm-login-app/src/location-picker/location-picker.test.tsx
+++ b/packages/apps/esm-login-app/src/location-picker/location-picker.test.tsx
@@ -8,6 +8,9 @@ import {
setSessionLocation,
setUserProperties,
showSnackbar,
+ LoggedInUser,
+ Session,
+ FetchResponse,
} from '@openmrs/esm-framework';
import {
mockLoginLocations,
@@ -31,30 +34,30 @@ const secondLocation = {
const invalidLocationUuid = '2gf1b7d4-c865-4178-82b0-5932e51503d6';
const userUuid = '90bd24b3-e700-46b0-a5ef-c85afdfededd';
-const mockedOpenmrsFetch = openmrsFetch as jest.Mock;
-const mockedUseConfig = useConfig as jest.Mock;
-const mockedUseSession = useSession as jest.Mock;
-
-mockedUseConfig.mockReturnValue(mockConfig);
-mockedUseSession.mockReturnValue({
- user: {
- display: 'Testy McTesterface',
- uuid: '90bd24b3-e700-46b0-a5ef-c85afdfededd',
- userProperties: {},
- },
-});
+const mockOpenmrsFetch = jest.mocked(openmrsFetch);
+const mockUseConfig = jest.mocked(useConfig);
+const mockUseSession = jest.mocked(useSession);
describe('LocationPickerView', () => {
beforeEach(() => {
- mockedOpenmrsFetch.mockImplementation((url) => {
+ mockUseConfig.mockReturnValue(mockConfig);
+
+ mockUseSession.mockReturnValue({
+ user: {
+ display: 'Testy McTesterface',
+ uuid: '90bd24b3-e700-46b0-a5ef-c85afdfededd',
+ userProperties: {},
+ } as LoggedInUser,
+ } as Session);
+
+ mockOpenmrsFetch.mockImplementation((url) => {
if (url === `/ws/fhir2/R4/Location?_id=${fistLocation.uuid}`) {
- return validatingLocationSuccessResponse;
+ return Promise.resolve(validatingLocationSuccessResponse as FetchResponse);
}
if (url === `/ws/fhir2/R4/Location?_id=${invalidLocationUuid}`) {
- return validatingLocationFailureResponse;
+ return Promise.resolve(validatingLocationFailureResponse as FetchResponse);
}
-
- return mockLoginLocations;
+ return Promise.resolve(mockLoginLocations as FetchResponse);
});
});
@@ -125,15 +128,16 @@ describe('LocationPickerView', () => {
it('should redirect to home if user preference in the userProperties is present and the location preference is valid', async () => {
const validLocationUuid = fistLocation.uuid;
- mockedUseSession.mockReturnValue({
+ mockUseSession.mockReturnValue({
user: {
display: 'Testy McTesterface',
uuid: userUuid,
userProperties: {
defaultLocation: validLocationUuid,
},
- },
- });
+ } as LoggedInUser,
+ } as Session);
+
await act(async () => {
renderWithRouter(LocationPickerView, {});
});
@@ -150,15 +154,15 @@ describe('LocationPickerView', () => {
});
it('should not redirect to home if user preference in the userProperties is present and the location preference is invalid', async () => {
- mockedUseSession.mockReturnValue({
+ mockUseSession.mockReturnValue({
user: {
display: 'Testy McTesterface',
uuid: userUuid,
userProperties: {
defaultLocation: invalidLocationUuid,
},
- },
- });
+ } as LoggedInUser,
+ } as Session);
await act(async () => {
renderWithRouter(LocationPickerView, {});
@@ -173,15 +177,15 @@ describe('LocationPickerView', () => {
describe('Testing updating user preference workflow', () => {
it('should not redirect if the login location page has a searchParam `update`', async () => {
- mockedUseSession.mockReturnValue({
+ mockUseSession.mockReturnValue({
user: {
display: 'Testy McTesterface',
uuid: userUuid,
userProperties: {
defaultLocation: fistLocation.uuid,
},
- },
- });
+ } as LoggedInUser,
+ } as Session);
await act(async () => {
renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
@@ -196,15 +200,15 @@ describe('LocationPickerView', () => {
it('should remove the saved preference if the login location page has a searchParam `update=true` and when submitting the user unchecks the checkbox ', async () => {
const user = userEvent.setup();
- mockedUseSession.mockReturnValue({
+ mockUseSession.mockReturnValue({
user: {
display: 'Testy McTesterface',
uuid: userUuid,
userProperties: {
defaultLocation: '1ce1b7d4-c865-4178-82b0-5932e51503d6',
},
- },
- });
+ } as LoggedInUser,
+ } as Session);
await act(async () => {
renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
@@ -240,15 +244,15 @@ describe('LocationPickerView', () => {
it('should update the user preference with new selection', async () => {
const user = userEvent.setup();
- mockedUseSession.mockReturnValue({
+ mockUseSession.mockReturnValue({
user: {
display: 'Testy McTesterface',
uuid: userUuid,
userProperties: {
defaultLocation: fistLocation.uuid,
},
- },
- });
+ } as LoggedInUser,
+ } as Session);
await act(async () => {
renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
@@ -281,15 +285,15 @@ describe('LocationPickerView', () => {
it('should not update the user preference with same selection', async () => {
const user = userEvent.setup();
- mockedUseSession.mockReturnValue({
+ mockUseSession.mockReturnValue({
user: {
display: 'Testy McTesterface',
uuid: userUuid,
userProperties: {
defaultLocation: fistLocation.uuid,
},
- },
- });
+ } as LoggedInUser,
+ } as Session);
await act(async () => {
renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
diff --git a/packages/apps/esm-login-app/src/login/login.test.tsx b/packages/apps/esm-login-app/src/login/login.test.tsx
index 5e0285e74..406067084 100644
--- a/packages/apps/esm-login-app/src/login/login.test.tsx
+++ b/packages/apps/esm-login-app/src/login/login.test.tsx
@@ -1,24 +1,29 @@
import { useState } from 'react';
import { waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { getSessionStore, refetchCurrentUser, useConfig, useSession } from '@openmrs/esm-framework';
+import { getSessionStore, refetchCurrentUser, type SessionStore, useConfig, useSession } from '@openmrs/esm-framework';
import { mockConfig } from '../../__mocks__/config.mock';
import renderWithRouter from '../test-helpers/render-with-router';
import Login from './login.component';
-const mockGetSessionStore = getSessionStore as jest.Mock;
-const mockedLogin = refetchCurrentUser as jest.Mock;
-const mockedUseConfig = useConfig as jest.Mock;
-const mockedUseSession = useSession as jest.Mock;
+const mockGetSessionStore = jest.mocked(getSessionStore);
+const mockLogin = jest.mocked(refetchCurrentUser);
+const mockUseConfig = jest.mocked(useConfig);
+const mockUseSession = jest.mocked(useSession);
-mockedLogin.mockReturnValue(Promise.resolve());
+mockLogin.mockResolvedValue({} as SessionStore);
mockGetSessionStore.mockImplementation(() => {
return {
getState: jest.fn().mockReturnValue({
+ loaded: true,
session: {
authenticated: true,
},
}),
+ setState: jest.fn(),
+ getInitialState: jest.fn(),
+ subscribe: jest.fn(),
+ destroy: jest.fn(),
};
});
@@ -27,8 +32,8 @@ const loginLocations = [
{ uuid: '222', display: 'Mars' },
];
-mockedUseSession.mockReturnValue({ authenticated: false });
-mockedUseConfig.mockReturnValue(mockConfig);
+mockUseSession.mockReturnValue({ authenticated: false, sessionId: '123' });
+mockUseConfig.mockReturnValue(mockConfig);
describe('Login', () => {
it('renders the login form', () => {
@@ -51,7 +56,7 @@ describe('Login', () => {
src: 'https://some-image-host.com/foo.png',
alt: 'Custom logo',
};
- mockedUseConfig.mockReturnValue({
+ mockUseConfig.mockReturnValue({
...mockConfig,
logo: customLogoConfig,
});
@@ -88,7 +93,7 @@ describe('Login', () => {
});
it('makes an API request when you submit the form', async () => {
- mockedLogin.mockReturnValue(Promise.resolve({ some: 'data' }));
+ mockLogin.mockResolvedValue({ some: 'data' } as unknown as SessionStore);
renderWithRouter(
Login,
@@ -99,7 +104,7 @@ describe('Login', () => {
);
const user = userEvent.setup();
- mockedLogin.mockClear();
+ mockLogin.mockClear();
await user.type(screen.getByRole('textbox', { name: /Username/i }), 'yoshi');
await user.click(screen.getByRole('button', { name: /Continue/i }));
@@ -113,16 +118,16 @@ describe('Login', () => {
// TODO: Complete the test
it('sends the user to the location select page on login if there is more than one location', async () => {
let refreshUser = (user: any) => {};
- mockedLogin.mockImplementation(() => {
+ mockLogin.mockImplementation(() => {
refreshUser({
display: 'my name',
});
- return Promise.resolve({ data: { authenticated: true } });
+ return Promise.resolve({ data: { authenticated: true } } as unknown as SessionStore);
});
- mockedUseSession.mockImplementation(() => {
+ mockUseSession.mockImplementation(() => {
const [user, setUser] = useState();
refreshUser = setUser;
- return { user, authenticated: !!user };
+ return { user, authenticated: !!user, sessionId: '123' };
});
renderWithRouter(
@@ -143,7 +148,7 @@ describe('Login', () => {
});
it('should render the both the username and password fields when the showPasswordOnSeparateScreen config is false', async () => {
- mockedUseConfig.mockReturnValue({
+ mockUseConfig.mockReturnValue({
...mockConfig,
showPasswordOnSeparateScreen: false,
});
@@ -168,7 +173,7 @@ describe('Login', () => {
});
it('should not render the password field when the showPasswordOnSeparateScreen config is true (default)', async () => {
- mockedUseConfig.mockReturnValue({
+ mockUseConfig.mockReturnValue({
...mockConfig,
});
@@ -192,13 +197,13 @@ describe('Login', () => {
});
it('should be able to login when the showPasswordOnSeparateScreen config is false', async () => {
- mockedLogin.mockReturnValue(Promise.resolve({ some: 'data' }));
- mockedUseConfig.mockReturnValue({
+ mockLogin.mockResolvedValue({ some: 'data' } as unknown as SessionStore);
+ mockUseConfig.mockReturnValue({
...mockConfig,
showPasswordOnSeparateScreen: false,
});
const user = userEvent.setup();
- mockedLogin.mockClear();
+ mockLogin.mockClear();
renderWithRouter(
Login,
@@ -220,7 +225,7 @@ describe('Login', () => {
});
it('should focus the username input', async () => {
- mockedUseConfig.mockReturnValue({
+ mockUseConfig.mockReturnValue({
...mockConfig,
});
@@ -238,7 +243,7 @@ describe('Login', () => {
it('should focus the password input in the password screen', async () => {
const user = userEvent.setup();
- mockedUseConfig.mockReturnValue({
+ mockUseConfig.mockReturnValue({
...mockConfig,
});
@@ -261,7 +266,7 @@ describe('Login', () => {
});
it('should focus the username input when the showPasswordOnSeparateScreen config is false', async () => {
- mockedUseConfig.mockReturnValue({
+ mockUseConfig.mockReturnValue({
...mockConfig,
showPasswordOnSeparateScreen: false,
});
diff --git a/packages/apps/esm-login-app/src/redirect-logout/logout.resource.ts b/packages/apps/esm-login-app/src/redirect-logout/logout.resource.ts
index 0d2ff70f4..0f5f4014f 100644
--- a/packages/apps/esm-login-app/src/redirect-logout/logout.resource.ts
+++ b/packages/apps/esm-login-app/src/redirect-logout/logout.resource.ts
@@ -1,5 +1,5 @@
-import { clearCurrentUser, openmrsFetch, refetchCurrentUser, restBaseUrl } from '@openmrs/esm-framework';
import { mutate } from 'swr';
+import { clearCurrentUser, openmrsFetch, refetchCurrentUser, restBaseUrl } from '@openmrs/esm-framework';
export async function performLogout() {
await openmrsFetch(`${restBaseUrl}/session`, {
diff --git a/packages/apps/esm-login-app/src/redirect-logout/redirect-logout.component.tsx b/packages/apps/esm-login-app/src/redirect-logout/redirect-logout.component.tsx
index fd65dfa0b..16d06d9a7 100644
--- a/packages/apps/esm-login-app/src/redirect-logout/redirect-logout.component.tsx
+++ b/packages/apps/esm-login-app/src/redirect-logout/redirect-logout.component.tsx
@@ -1,36 +1,40 @@
-import type React from 'react';
import { useEffect } from 'react';
import { navigate, setUserLanguage, useConfig, useConnectivity, useSession } from '@openmrs/esm-framework';
import { clearHistory } from '@openmrs/esm-framework/src/internal';
+import { type ConfigSchema } from '../config-schema';
import { performLogout } from './logout.resource';
-export interface RedirectLogoutProps {}
-
-const RedirectLogout: React.FC = () => {
- const config = useConfig();
- const session = useSession();
+const RedirectLogout: React.FC = () => {
+ const config = useConfig();
const isLoginEnabled = useConnectivity();
+ const session = useSession();
useEffect(() => {
clearHistory();
if (!session.authenticated || !isLoginEnabled) {
navigate({ to: '${openmrsSpaBase}/login' });
} else {
- performLogout().then(() => {
- const defaultLang = document.documentElement.getAttribute('data-default-lang');
- setUserLanguage({
- locale: defaultLang,
- authenticated: false,
- sessionId: '',
+ performLogout()
+ .then(() => {
+ const defaultLanguage = document.documentElement.getAttribute('data-default-lang');
+
+ setUserLanguage({
+ locale: defaultLanguage,
+ authenticated: false,
+ sessionId: '',
+ });
+
+ if (config.provider.type === 'oauth2') {
+ navigate({ to: config.provider.logoutUrl });
+ } else {
+ navigate({ to: '${openmrsSpaBase}/login' });
+ }
+ })
+ .catch((error) => {
+ console.error('Logout failed:', error);
});
- if (config.provider.type === 'oauth2') {
- navigate({ to: config.provider.logoutUrl });
- } else {
- navigate({ to: '${openmrsSpaBase}/login' });
- }
- });
}
- }, [isLoginEnabled, session, config]);
+ }, [config, isLoginEnabled, session]);
return null;
};
diff --git a/packages/apps/esm-login-app/src/redirect-logout/redirect-logout.test.tsx b/packages/apps/esm-login-app/src/redirect-logout/redirect-logout.test.tsx
index 9315099ba..3003d143f 100644
--- a/packages/apps/esm-login-app/src/redirect-logout/redirect-logout.test.tsx
+++ b/packages/apps/esm-login-app/src/redirect-logout/redirect-logout.test.tsx
@@ -1,7 +1,8 @@
import React from 'react';
+import { mutate } from 'swr';
import { render, waitFor } from '@testing-library/react';
-import RedirectLogout from './redirect-logout.component';
import {
+ type FetchResponse,
type Session,
clearCurrentUser,
navigate,
@@ -13,81 +14,154 @@ import {
useConnectivity,
useSession,
} from '@openmrs/esm-framework';
-import { mutate } from 'swr';
+import RedirectLogout from './redirect-logout.component';
jest.mock('swr', () => ({
mutate: jest.fn(),
}));
-Object.defineProperty(document, 'documentElement', {
- value: {
- getAttribute: jest.fn().mockReturnValue('km'),
- },
-});
+const mockClearCurrentUser = jest.mocked(clearCurrentUser);
+const mockNavigate = jest.mocked(navigate);
+const mockOpenmrsFetch = jest.mocked(openmrsFetch);
+const mockRefetchCurrentUser = jest.mocked(refetchCurrentUser);
+const mockSetUserLanguage = jest.mocked(setUserLanguage);
+const mockUseConfig = jest.mocked(useConfig);
+const mockUseConnectivity = jest.mocked(useConnectivity);
+const mockUseSession = jest.mocked(useSession);
-describe('Testing Logout', () => {
+describe('RedirectLogout', () => {
beforeEach(() => {
- (useConnectivity as jest.Mock).mockReturnValue(true);
- (openmrsFetch as jest.Mock).mockResolvedValue({});
- (useSession as jest.Mock).mockReturnValue({
+ mockUseConnectivity.mockReturnValue(true);
+ mockOpenmrsFetch.mockResolvedValue({} as FetchResponse);
+
+ mockUseSession.mockReturnValue({
authenticated: true,
sessionId: 'xyz',
} as Session);
- (useConfig as jest.Mock).mockReturnValue({
+
+ mockUseConfig.mockReturnValue({
provider: {
type: '',
},
});
+
+ Object.defineProperty(document, 'documentElement', {
+ configurable: true,
+ value: {
+ getAttribute: jest.fn().mockReturnValue('km'),
+ },
+ });
});
- it('should render Logout and redirect to login page', async () => {
+
+ it('should redirect to login page upon logout', async () => {
render();
- expect(openmrsFetch).toHaveBeenCalledWith(`${restBaseUrl}/session`, {
+
+ expect(mockOpenmrsFetch).toHaveBeenCalledWith(`${restBaseUrl}/session`, {
method: 'DELETE',
});
+
await waitFor(() => expect(mutate).toHaveBeenCalled());
- expect(clearCurrentUser).toHaveBeenCalled();
- expect(refetchCurrentUser).toHaveBeenCalled();
- expect(setUserLanguage).toHaveBeenCalledWith({
+
+ expect(mockClearCurrentUser).toHaveBeenCalled();
+ expect(mockRefetchCurrentUser).toHaveBeenCalled();
+ expect(mockSetUserLanguage).toHaveBeenCalledWith({
locale: 'km',
authenticated: false,
sessionId: '',
});
- expect(navigate).toHaveBeenCalledWith({ to: '${openmrsSpaBase}/login' });
+ expect(mockNavigate).toHaveBeenCalledWith({ to: '${openmrsSpaBase}/login' });
});
- it('should render Logout and redirect to provider.logoutUrl if provider.type === oauth2', async () => {
- (useConfig as jest.Mock).mockReturnValue({
+ it('should redirect to `provider.logoutUrl` if the configured provider is `oauth2`', async () => {
+ mockUseConfig.mockReturnValue({
provider: {
type: 'oauth2',
logoutUrl: '/oauth/logout',
},
});
+
render();
- expect(openmrsFetch).toHaveBeenCalledWith(`${restBaseUrl}/session`, {
+
+ expect(mockOpenmrsFetch).toHaveBeenCalledWith(`${restBaseUrl}/session`, {
method: 'DELETE',
});
+
await waitFor(() => expect(mutate).toHaveBeenCalled());
- expect(clearCurrentUser).toHaveBeenCalled();
- expect(refetchCurrentUser).toHaveBeenCalled();
- expect(setUserLanguage).toHaveBeenCalledWith({
+
+ expect(mockClearCurrentUser).toHaveBeenCalled();
+ expect(mockRefetchCurrentUser).toHaveBeenCalled();
+ expect(mockSetUserLanguage).toHaveBeenCalledWith({
locale: 'km',
authenticated: false,
sessionId: '',
});
- expect(navigate).toHaveBeenCalledWith({ to: '/oauth/logout' });
+ expect(mockNavigate).toHaveBeenCalledWith({ to: '/oauth/logout' });
});
it('should redirect to login if the session is already unauthenticated', async () => {
- (useSession as jest.Mock).mockReturnValue({
+ mockUseSession.mockReturnValue({
authenticated: false,
} as Session);
+
render();
- expect(navigate).toHaveBeenCalledWith({ to: '${openmrsSpaBase}/login' });
+
+ expect(mockNavigate).toHaveBeenCalledWith({ to: '${openmrsSpaBase}/login' });
});
- it('should redirect to login if the application is Offline', async () => {
- (useConnectivity as jest.Mock).mockReturnValue(false);
+ it('should redirect to login if the application is offline', async () => {
+ mockUseConnectivity.mockReturnValue(false);
+
render();
- expect(navigate).toHaveBeenCalledWith({ to: '${openmrsSpaBase}/login' });
+
+ expect(mockNavigate).toHaveBeenCalledWith({ to: '${openmrsSpaBase}/login' });
+ });
+
+ it('should handle logout failure gracefully', async () => {
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
+ mockOpenmrsFetch.mockRejectedValue(new Error('Logout failed'));
+
+ render();
+
+ await waitFor(() => {
+ expect(consoleError).toHaveBeenCalledWith('Logout failed:', new Error('Logout failed'));
+ });
+
+ consoleError.mockRestore();
+ });
+
+ it('should handle missing default language attribute', async () => {
+ Object.defineProperty(document, 'documentElement', {
+ configurable: true,
+ value: {
+ getAttribute: jest.fn().mockReturnValue(null),
+ },
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(mockSetUserLanguage).toHaveBeenCalledWith({
+ locale: null,
+ authenticated: false,
+ sessionId: '',
+ });
+ });
+ });
+
+ it('should handle config changes appropriately', async () => {
+ const { rerender } = render();
+
+ mockUseConfig.mockReturnValue({
+ provider: {
+ type: 'oauth2',
+ logoutUrl: '/new/logout/url',
+ },
+ });
+
+ rerender();
+
+ await waitFor(() => {
+ expect(mockNavigate).toHaveBeenCalledWith({ to: '/new/logout/url' });
+ });
});
});
diff --git a/packages/apps/esm-primary-navigation-app/src/components/change-language/change-language.test.tsx b/packages/apps/esm-primary-navigation-app/src/components/change-language/change-language.test.tsx
index 8089f0794..4097cbea8 100644
--- a/packages/apps/esm-primary-navigation-app/src/components/change-language/change-language.test.tsx
+++ b/packages/apps/esm-primary-navigation-app/src/components/change-language/change-language.test.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
-import { useSession } from '@openmrs/esm-framework';
+import { type LoggedInUser, type Session, useConnectivity, useSession } from '@openmrs/esm-framework';
import ChangeLanguageModal from './change-language.modal';
const mockUser = {
@@ -11,32 +11,95 @@ const mockUser = {
},
};
-const mockUseSession = useSession as jest.Mock;
-mockUseSession.mockReturnValue({
- authenticated: true,
- user: mockUser,
- allowedLocales: ['en', 'fr', 'it', 'pt'],
- locale: 'fr',
-});
-
const mockPostUserPropertiesOnline = jest.fn((...args) => Promise.resolve());
+const mockPostUserPropertiesOffline = jest.fn((...args) => Promise.resolve());
+const mockUseConnectivity = jest.mocked(useConnectivity);
+const mockUseSession = jest.mocked(useSession);
+
jest.mock('./change-language.resource', () => ({
postUserPropertiesOnline: (...args) => mockPostUserPropertiesOnline(...args),
- postUserPropertiesOffline: jest.fn(),
+ postUserPropertiesOffline: (...args) => mockPostUserPropertiesOffline(...args),
}));
describe(`Change Language Modal`, () => {
- it('should change user locale', async () => {
+ beforeEach(() => {
+ mockUseSession.mockReturnValue({
+ authenticated: true,
+ user: mockUser as unknown as LoggedInUser,
+ allowedLocales: ['en', 'fr', 'it', 'pt'],
+ locale: 'fr',
+ } as Session);
+ });
+
+ it('should correctly displays all allowed locales', () => {
+ render();
+
+ expect(screen.getByRole('radio', { name: /english/i })).toBeInTheDocument();
+ expect(screen.getByRole('radio', { name: /français/i })).toBeInTheDocument();
+ expect(screen.getByRole('radio', { name: /italiano/i })).toBeInTheDocument();
+ expect(screen.getByRole('radio', { name: /português/i })).toBeInTheDocument();
+ });
+
+ it('should close the modal when the cancel button is clicked', async () => {
+ const user = userEvent.setup();
+ const mockClose = jest.fn();
+
+ render();
+
+ await user.click(screen.getByRole('button', { name: /cancel/i }));
+ expect(mockClose).toHaveBeenCalled();
+ });
+
+ it('should change user locale when the submit button is clicked', async () => {
const user = userEvent.setup();
render();
+
expect(screen.getByRole('radio', { name: /français/ })).toBeChecked();
+
await user.click(screen.getByRole('radio', { name: /english/i }));
await user.click(screen.getByRole('button', { name: /change/i }));
+
expect(mockPostUserPropertiesOnline).toHaveBeenCalledWith(
mockUser.uuid,
{ defaultLocale: 'en' },
expect.anything(),
);
});
+
+ it('should show a loading indicator in the submit button while language change is in progress', async () => {
+ const user = userEvent.setup();
+ mockPostUserPropertiesOnline.mockImplementation(() => new Promise(() => {}));
+
+ render();
+
+ await user.click(screen.getByRole('radio', { name: /english/i }));
+ await user.click(screen.getByRole('button', { name: /change/i }));
+
+ expect(screen.getByText(/changing language.../i)).toBeInTheDocument();
+ });
+
+ it('should use offline endpoint when user is offline', async () => {
+ const user = userEvent.setup();
+ mockUseConnectivity.mockReturnValue(false);
+
+ render();
+
+ await user.click(screen.getByRole('radio', { name: /english/i }));
+ await user.click(screen.getByRole('button', { name: /change/i }));
+
+ expect(mockPostUserPropertiesOffline).toHaveBeenCalledWith(
+ mockUser.uuid,
+ { defaultLocale: 'en' },
+ expect.anything(),
+ );
+ expect(mockPostUserPropertiesOnline).not.toHaveBeenCalled();
+ });
+
+ it('should disable submit button when selected locale is same as current locale', () => {
+ render();
+
+ const submitButton = screen.getByRole('button', { name: /change/i });
+ expect(submitButton).toBeDisabled();
+ });
});
diff --git a/packages/apps/esm-primary-navigation-app/src/components/logo/logo.component.tsx b/packages/apps/esm-primary-navigation-app/src/components/logo/logo.component.tsx
index 011d0714c..30f4060ad 100644
--- a/packages/apps/esm-primary-navigation-app/src/components/logo/logo.component.tsx
+++ b/packages/apps/esm-primary-navigation-app/src/components/logo/logo.component.tsx
@@ -1,17 +1,23 @@
import React from 'react';
import { interpolateUrl, useConfig } from '@openmrs/esm-framework';
+import { type ConfigSchema } from '../../config-schema';
import styles from './logo.scss';
const Logo: React.FC = () => {
- const { logo } = useConfig();
+ const { logo } = useConfig();
+
+ const handleImageError = (e: React.SyntheticEvent) => {
+ console.error('Failed to load logo image:', e);
+ };
+
return (
<>
{logo?.src ? (
-
+
) : logo?.name ? (
logo.name
) : (
-