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.alt} + {logo.alt} ) : logo?.name ? ( logo.name ) : ( - + )} diff --git a/packages/apps/esm-primary-navigation-app/src/components/logo/logo.test.tsx b/packages/apps/esm-primary-navigation-app/src/components/logo/logo.test.tsx index e143dfe52..84cbb94d0 100644 --- a/packages/apps/esm-primary-navigation-app/src/components/logo/logo.test.tsx +++ b/packages/apps/esm-primary-navigation-app/src/components/logo/logo.test.tsx @@ -1,31 +1,36 @@ import React from 'react'; -import { useConfig } from '@openmrs/esm-framework'; +import userEvent from '@testing-library/user-event'; import { render, screen } from '@testing-library/react'; +import { useConfig } from '@openmrs/esm-framework'; +import { type ConfigSchema } from '../../config-schema'; import Logo from './logo.component'; -const mockUseConfig = useConfig as jest.Mock; +// FIXME: Figure out why I can't annotate this mock like so: jest.mocked(useConfig); +const mockUseConfig = jest.mocked(useConfig); -jest.mock('@openmrs/esm-framework', () => ({ - useConfig: jest.fn(), - interpolateUrl: jest.fn(), -})); - -describe('', () => { - it('should display OpenMRS logo', () => { +describe('Logo', () => { + it('should display the OpenMRS logo by default', () => { const mockConfig = { logo: { src: null, alt: null, name: null } }; - mockUseConfig.mockReturnValue(mockConfig); + mockUseConfig.mockReturnValue(mockConfig as ConfigSchema); + render(); + const logo = screen.getByRole('img'); + expect(logo).toBeInTheDocument(); expect(logo).toContainHTML('svg'); }); it('should display name', () => { const mockConfig = { - logo: { src: null, alt: null, name: 'Some weird EMR' }, + logo: { src: null, alt: null, name: 'Some weird EMR', link: null }, + externalRefLinks: null, }; - mockUseConfig.mockReturnValue(mockConfig); + + mockUseConfig.mockReturnValue(mockConfig as ConfigSchema); + render(); + expect(screen.getByText(/Some weird EMR/i)).toBeInTheDocument(); }); @@ -37,10 +42,38 @@ describe('', () => { name: null, }, }; + mockUseConfig.mockReturnValue(mockConfig); + render(); + const logo = screen.getByRole('img'); + expect(logo).toBeInTheDocument(); expect(logo).toHaveAttribute('alt'); }); + + it('should handle image load errors', () => { + const user = userEvent.setup(); + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + const mockConfig = { + logo: { + src: 'invalid-image.png', + alt: 'alt text', + name: null, + }, + }; + + mockUseConfig.mockReturnValue(mockConfig); + + render(); + + const img = screen.getByRole('img'); + + const errorEvent = new Event('error'); + img.dispatchEvent(errorEvent); + + expect(consoleSpy).toHaveBeenCalledWith('Failed to load logo image:', expect.any(Object)); + consoleSpy.mockRestore(); + }); }); diff --git a/packages/apps/esm-primary-navigation-app/src/components/user-panel-switcher-item/user-panel-switcher.test.tsx b/packages/apps/esm-primary-navigation-app/src/components/user-panel-switcher-item/user-panel-switcher.test.tsx index 4bcd0046a..50e8432b9 100644 --- a/packages/apps/esm-primary-navigation-app/src/components/user-panel-switcher-item/user-panel-switcher.test.tsx +++ b/packages/apps/esm-primary-navigation-app/src/components/user-panel-switcher-item/user-panel-switcher.test.tsx @@ -1,21 +1,22 @@ import React from 'react'; import { screen, render } from '@testing-library/react'; -import { useSession } from '@openmrs/esm-framework'; -import UserPanelSwitcher from './user-panel-switcher.component'; +import { type LoggedInUser, type Session, useSession } from '@openmrs/esm-framework'; import { mockLoggedInUser } from '../../../__mocks__/mock-user'; +import UserPanelSwitcher from './user-panel-switcher.component'; -const mockUseSession = useSession as jest.Mock; +const mockUseSession = jest.mocked(useSession); -describe('', () => { +describe('UserPanelSwitcher', () => { beforeEach(() => { mockUseSession.mockReturnValue({ authenticated: true, - user: mockLoggedInUser, - }); + user: mockLoggedInUser as unknown as LoggedInUser, + } as unknown as Session); }); it('should display user name', async () => { render(); + expect(await screen.findByText(/Dr Healther Morgan/i)).toBeInTheDocument(); }); }); diff --git a/packages/apps/esm-primary-navigation-app/src/config-schema.ts b/packages/apps/esm-primary-navigation-app/src/config-schema.ts index 242938c87..073d5032d 100644 --- a/packages/apps/esm-primary-navigation-app/src/config-schema.ts +++ b/packages/apps/esm-primary-navigation-app/src/config-schema.ts @@ -41,3 +41,13 @@ export const configSchema = { _description: 'The external links to be showcased in the app menu', }, }; + +export type ConfigSchema = { + logo: { + src: string | null; + alt: string; + name: string | null; + link: string | null; + }; + externalRefLinks: { title: string; redirect: string }[]; +}; diff --git a/packages/apps/esm-primary-navigation-app/src/root.component.test.tsx b/packages/apps/esm-primary-navigation-app/src/root.component.test.tsx index 6fce3f460..c6d40a201 100644 --- a/packages/apps/esm-primary-navigation-app/src/root.component.test.tsx +++ b/packages/apps/esm-primary-navigation-app/src/root.component.test.tsx @@ -2,7 +2,13 @@ import React from 'react'; import { of } from 'rxjs'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { useConfig, useAssignedExtensions, useSession } from '@openmrs/esm-framework'; +import { + useConfig, + useAssignedExtensions, + useSession, + type AssignedExtension, + type Session, +} from '@openmrs/esm-framework'; import { isDesktop } from './utils'; import { mockUser } from '../__mocks__/mock-user'; import { mockSession } from '../__mocks__/mock-session'; @@ -12,15 +18,15 @@ const mockUserObservable = of(mockUser); const mockSessionObservable = of({ data: mockSession }); const mockIsDesktop = jest.mocked(isDesktop); -const mockedUseConfig = useConfig as jest.Mock; -const mockedUseAssignedExtensions = useAssignedExtensions as jest.Mock; -const mockedUseSession = useSession as jest.Mock; +const mockUseConfig = jest.mocked(useConfig); +const mockUseAssignedExtensions = jest.mocked(useAssignedExtensions); +const mockUseSession = jest.mocked(useSession); -mockedUseConfig.mockReturnValue({ +mockUseConfig.mockReturnValue({ logo: { src: null, alt: null, name: 'Mock EMR', link: 'Mock EMR' }, }); -mockedUseAssignedExtensions.mockReturnValue(['mock-extension']); -mockedUseSession.mockReturnValue(mockSession); +mockUseAssignedExtensions.mockReturnValue(['mock-extension'] as unknown as AssignedExtension[]); +mockUseSession.mockReturnValue(mockSession as unknown as Session); jest.mock('./root.resource', () => ({ getSynchronizedCurrentUser: jest.fn(() => mockUserObservable), diff --git a/packages/framework/esm-api/src/openmrs-fetch.test.ts b/packages/framework/esm-api/src/openmrs-fetch.test.ts index 55b3a7a07..e27ea3703 100644 --- a/packages/framework/esm-api/src/openmrs-fetch.test.ts +++ b/packages/framework/esm-api/src/openmrs-fetch.test.ts @@ -1,8 +1,7 @@ -import { openmrsFetch, openmrsObservableFetch } from './openmrs-fetch'; import { isObservable } from 'rxjs'; - import { getConfig as mockGetConfig } from '@openmrs/esm-config'; import { navigate as mockNavigate } from '@openmrs/esm-navigation'; +import { openmrsFetch, openmrsObservableFetch } from './openmrs-fetch'; describe('openmrsFetch', () => { beforeEach(() => { @@ -25,28 +24,28 @@ describe('openmrsFetch', () => { }); it(`throws an error if you don't pass in a url string`, () => { - // @ts-ignore + // @ts-expect-error expect(() => openmrsFetch()).toThrow(/first argument/); - // @ts-ignore + // @ts-expect-error expect(() => openmrsFetch({})).toThrow(/first argument/); }); - it(`throws an error if you pass in an invalid fetchInit object`, () => { - // @ts-ignore + it('throws an error if you pass in an invalid fetchInit object', () => { + // @ts-expect-error expect(() => openmrsFetch('/session', 'invalid second arg')).toThrow(/second argument/); - // @ts-ignore + // @ts-expect-error expect(() => openmrsFetch('/session', 123)).toThrow(/second argument/); }); - it(`throws an Error if there is no openmrsBase`, () => { + it('throws an Error if there is no openmrsBase', () => { // @ts-ignore delete window.openmrsBase; expect(() => openmrsFetch('/session')).toThrow(/openmrsBase/); }); - it(`calls window.fetch with the correct arguments for a basic GET request`, () => { + it('calls window.fetch with the correct arguments for a basic GET request', () => { // @ts-ignore window.fetch.mockReturnValue(new Promise(() => {})); openmrsFetch('/ws/rest/v1/session'); @@ -58,7 +57,7 @@ describe('openmrsFetch', () => { }); }); - it(`calls window.fetch correctly for requests that have a request body`, () => { + it('calls window.fetch correctly for requests that have a request body', () => { // @ts-ignore window.fetch.mockReturnValue(new Promise(() => {})); const requestBody = { some: 'json' }; @@ -76,7 +75,7 @@ describe('openmrsFetch', () => { }); }); - it(`allows you to specify your own Accept request header`, () => { + it('allows you to specify your own Accept request header', () => { // @ts-ignore window.fetch.mockReturnValue(new Promise(() => {})); const requestBody = { some: 'json' }; @@ -93,7 +92,7 @@ describe('openmrsFetch', () => { }); }); - it(`allows you to specify no Accept request header to be sent`, () => { + it('allows you to specify no Accept request header to be sent', () => { // @ts-ignore window.fetch.mockReturnValue(new Promise(() => {})); openmrsFetch('/ws/rest/v1/session', { @@ -109,7 +108,7 @@ describe('openmrsFetch', () => { }); }); - it(`returns a promise that resolves with a json object when the request succeeds`, () => { + it('returns a promise that resolves with a json object when the request succeeds', () => { // @ts-ignore window.fetch.mockReturnValue( Promise.resolve({ @@ -127,7 +126,7 @@ describe('openmrsFetch', () => { }); }); - it(`returns a promise that resolves with null when the request succeeds with HTTP 204`, () => { + it('returns a promise that resolves with null when the request succeeds with HTTP 204', () => { // @ts-ignore window.fetch.mockReturnValue( Promise.resolve({ @@ -143,7 +142,7 @@ describe('openmrsFetch', () => { }); }); - it(`gives you an amazing error when the server responds with a 500 that has json`, () => { + it('gives you an amazing error when the server responds with a 500 that has json', () => { // @ts-ignore window.fetch.mockReturnValue( Promise.resolve({ @@ -173,7 +172,7 @@ describe('openmrsFetch', () => { }); }); - it(`gives you an amazing error when the server responds with a 400 that doesn't have json`, () => { + it("gives you an amazing error when the server responds with a 400 that doesn't have json", () => { // @ts-ignore window.fetch.mockReturnValue( Promise.resolve({ @@ -198,7 +197,7 @@ describe('openmrsFetch', () => { }); }); - it(`navigates to spa login page when the server responds with a 401`, () => { + it('navigates to spa login page when the server responds with a 401', () => { (mockGetConfig as any).mockResolvedValueOnce({ redirectAuthFailure: { enabled: true, @@ -234,7 +233,7 @@ describe('openmrsObservableFetch', () => { window.fetch = jest.fn(); }); - it(`calls window.fetch with the correct arguments for a basic GET request`, (done) => { + it('calls window.fetch with the correct arguments for a basic GET request', (done) => { // @ts-ignore window.fetch.mockReturnValue( Promise.resolve({ @@ -266,7 +265,7 @@ describe('openmrsObservableFetch', () => { expect(window.fetch.mock.calls[0][1].headers.Accept).toEqual('application/json'); }); - it(`aborts the fetch request when subscription is unsubscribed`, () => { + it('aborts the fetch request when subscription is unsubscribed', () => { // @ts-ignore window.fetch.mockReturnValue(new Promise(() => {})); diff --git a/packages/framework/esm-config/src/setup-tests.js b/packages/framework/esm-config/src/setup-tests.js index f068b38fd..1496809b9 100644 --- a/packages/framework/esm-config/src/setup-tests.js +++ b/packages/framework/esm-config/src/setup-tests.js @@ -1,4 +1,5 @@ -window.System = { +/* eslint-disable no-undef */ +global.window.System = { import: jest.fn().mockRejectedValue(new Error('config.json not available in import map')), resolve: jest.fn().mockImplementation(() => { throw new Error('config.json not available in import map'); @@ -6,6 +7,6 @@ window.System = { register: jest.fn(), }; -window.openmrsBase = '/openmrs'; -window.spaBase = '/spa'; -window.getOpenmrsSpaBase = () => '/openmrs/spa/'; +global.window.openmrsBase = '/openmrs'; +global.window.spaBase = '/spa'; +global.window.getOpenmrsSpaBase = () => '/openmrs/spa/'; diff --git a/packages/framework/esm-config/src/validators/type-validators.test.ts b/packages/framework/esm-config/src/validators/type-validators.test.ts index 1f7b8a23c..4898a1f8e 100644 --- a/packages/framework/esm-config/src/validators/type-validators.test.ts +++ b/packages/framework/esm-config/src/validators/type-validators.test.ts @@ -1,4 +1,4 @@ -import { isString, isBoolean, isUuid, isObject, isNumber } from './type-validators'; +import { isString, isBoolean, isUuid, isObject, isNumber, isArray } from './type-validators'; describe('all validators', () => { it('fail on undefined', () => { @@ -28,6 +28,15 @@ describe('isNumber', () => { it('rejects non-numbers', () => { expect(isNumber('Not a Number')).toMatch('must be a number'); }); + + it('accepts zero and negative numbers', () => { + expect(isNumber(0)).toBeUndefined(); + expect(isNumber(-1)).toBeUndefined(); + }); + + it('accepts floating point numbers', () => { + expect(isNumber(3.14)).toBeUndefined(); + }); }); describe('isBoolean', () => { @@ -56,6 +65,11 @@ describe('isUuid', () => { it('rejects a bad CIEL External ID', () => { expect(isUuid('123118AAAAAAAAAAAAAAAAAAAAAAAAAAAAA')).toMatch('must be a valid UUID'); }); + + it('rejects UUIDs with wrong segment lengths', () => { + expect(isUuid('28c37ff6-0079-4fa7-b803-5d547ac454e0a')).toMatch('must be a valid UUID'); + expect(isUuid('28c37ff6-0079-4fa7-b803-5d547ac454')).toMatch('must be a valid UUID'); + }); }); describe('isObject', () => { @@ -71,4 +85,26 @@ describe('isObject', () => { it('rejects null', () => { expect(isObject(null)).toMatch(/must be an object/i); }); + + it('rejects primitive types', () => { + expect(isObject(42)).toMatch(/must be an object/i); + expect(isObject('string')).toMatch(/must be an object/i); + expect(isObject(true)).toMatch(/must be an object/i); + }); + + it('rejects functions', () => { + expect(isObject(() => {})).toMatch(/must be an object/i); + }); +}); + +describe('isArray', () => { + it('accepts arrays', () => { + expect(isArray([])).toBeUndefined(); + expect(isArray([1, 2, 3])).toBeUndefined(); + }); + + it('rejects non-arrays', () => { + expect(isArray({})).toMatch('must be an array'); + expect(isArray('not an array')).toMatch('must be an array'); + }); }); diff --git a/packages/framework/esm-error-handling/src/openmrs-esm-error-handling.test.js b/packages/framework/esm-error-handling/src/openmrs-esm-error-handling.test.ts similarity index 66% rename from packages/framework/esm-error-handling/src/openmrs-esm-error-handling.test.js rename to packages/framework/esm-error-handling/src/openmrs-esm-error-handling.test.ts index f7a6b13f1..faf62941d 100644 --- a/packages/framework/esm-error-handling/src/openmrs-esm-error-handling.test.js +++ b/packages/framework/esm-error-handling/src/openmrs-esm-error-handling.test.ts @@ -1,9 +1,14 @@ +import { jest, describe, it, expect } from '@jest/globals'; +import type { reportError as ReportErrorType } from './index'; + jest.mock('./index'); -const { reportError } = jest.requireActual('./index'); + +const { reportError } = jest.requireActual('./index') as { reportError: typeof ReportErrorType }; jest.useFakeTimers(); + describe('error handler', () => { - it('transfrom the input in valid error object if it is not already an error obejct', () => { + it('transforms non-Error inputs into valid Error objects', () => { expect(() => { reportError('error'); jest.runAllTimers(); diff --git a/packages/framework/esm-expression-evaluator/src/evaluator.test.ts b/packages/framework/esm-expression-evaluator/src/evaluator.test.ts index da0ec8e04..00fab3d87 100644 --- a/packages/framework/esm-expression-evaluator/src/evaluator.test.ts +++ b/packages/framework/esm-expression-evaluator/src/evaluator.test.ts @@ -2,26 +2,26 @@ import { describe, it, expect } from '@jest/globals'; import { compile, evaluate, evaluateAsBoolean, evaluateAsNumber, evaluateAsType, evaluateAsync } from './evaluator'; describe('OpenMRS Expression Evaluator', () => { - it('Should evaluate a simple expression', () => { + it('should evaluate a simple expression', () => { expect(evaluate('1 + 1')).toBe(2); }); - it('Should support multiplication', () => { + it('should support multiplication', () => { expect(evaluate('1 * 2')).toBe(2); }); - it('Should support the not operator', () => { + it('should support the not operator', () => { expect(evaluate('!1')).toBe(false); expect(evaluate('!0')).toBe(true); expect(evaluate('!true')).toBe(false); expect(evaluate('!false')).toBe(true); }); - it('Should support expressions in parentheses', () => { + it('should support expressions in parentheses', () => { expect(evaluate('(1, 2, 3)')).toBe(3); }); - it('Should support order of operations', () => { + it('should support order of operations', () => { expect(evaluate('(1 + 2) * 3')).toBe(9); expect(evaluate('1 + 2 * 3')).toBe(7); }); @@ -34,26 +34,26 @@ describe('OpenMRS Expression Evaluator', () => { expect(evaluate('1 in [1, 2, 3]')).toBe(true); }); - it('Should support basic variables', () => { + it('should support basic variables', () => { expect(evaluate('1 + a', { a: 3 })).toBe(4); }); - it('Should support nullish coalescing', () => { + it('should support nullish coalescing', () => { expect(evaluate('a ?? b', { a: null, b: 3 })).toBe(3); expect(evaluate('a ?? b', { a: 3, b: null })).toBe(3); }); - it('Should support functions', () => { + it('should support functions', () => { expect(evaluate('a(1)', { a: (i: number) => i + 1 })).toBe(2); }); - it('Should support built-in functions', () => { + it('should support built-in functions', () => { expect(evaluate('a.includes("v")', { a: 'value' })).toBe(true); expect(evaluate('"value".includes("v")')).toBe(true); expect(evaluate('(3.14159).toPrecision(3)')).toBe('3.14'); }); - it('Should give a useful error message for properties on missing objects', () => { + it('should give a useful error message for properties on missing objects', () => { expect(() => evaluate('a.b')).toThrow('ReferenceError: a is not defined'); expect(() => evaluate('a["b"]')).toThrow('ReferenceError: a is not defined'); expect(() => evaluate('a.b.c', { a: {} })).toThrow("TypeError: cannot read properties of undefined (reading 'c')"); @@ -65,17 +65,17 @@ describe('OpenMRS Expression Evaluator', () => { ); }); - it('Should not support this', () => { + it('should not support this', () => { expect(() => evaluate('this')).toThrow( /Expression evaluator does not support expression of type 'ThisExpression'.*/i, ); }); - it('Should support property references', () => { + it('should support property references', () => { expect(evaluate('a.b.c', { a: { b: { c: 3 } } })).toBe(3); }); - it('Should not support prototype references', () => { + it('should not support prototype references', () => { expect(() => evaluate('a.__proto__', { a: {} })).toThrow(/Cannot access the __proto__ property .*/i); expect(() => evaluate('a["__proto__"]', { a: {} })).toThrow(/Cannot access the __proto__ property .*/i); expect(() => evaluate('a[b]', { a: {}, b: '__proto__' })).toThrow(/Cannot access the __proto__ property .*/i); @@ -88,36 +88,36 @@ describe('OpenMRS Expression Evaluator', () => { expect(() => evaluate('a[b]', { a: {}, b: 'prototype' })).toThrow(/Cannot access the prototype property .*/i); }); - it('Should support ternaries', () => { + it('should support ternaries', () => { expect(evaluate('a ? 1 : 2', { a: true })).toBe(1); expect(evaluate('a ? 1 : 2', { a: false })).toBe(2); }); - it('Should support hexadecimal', () => { + it('should support hexadecimal', () => { expect(evaluate('0xff')).toBe(255); }); - it('Should support string templates', () => { + it('should support string templates', () => { expect(evaluate('`${a.b}`', { a: { b: 'string' } })).toBe('string'); }); - it('Should support new Date()', () => { + it('should support new Date()', () => { expect(evaluate('new Date().getTime()')).toBeLessThanOrEqual(new Date().getTime()); }); - it('Should support RegExp', () => { + it('should support RegExp', () => { expect(evaluate('/.*/.test(a)', { a: 'a' })).toBe(true); }); - it('Should support RegExp objects', () => { + it('should support RegExp objects', () => { expect(evaluate('new RegExp(".*").test(a)', { a: 'a' })).toBe(true); }); - it('Should support arrow functions inside expressions', () => { + it('should support arrow functions inside expressions', () => { expect(evaluate('[1, 2, 3].find(v => v === 3)')).toBe(3); }); - it('Should support globals', () => { + it('should support globals', () => { expect(evaluate('NaN')).toBeNaN(); expect(evaluate('Infinity')).toBe(Infinity); expect(evaluate('Boolean(true)')).toBe(true); @@ -127,7 +127,7 @@ describe('OpenMRS Expression Evaluator', () => { expect(evaluate('Number.isInteger(42)')).toBe(true); }); - it('Should not support creating arbitrary objects', () => { + it('should not support creating arbitrary objects', () => { expect(() => evaluate('new object()')).toThrow(/Cannot instantiate object .*/i); class Fn { constructor() {} @@ -135,12 +135,12 @@ describe('OpenMRS Expression Evaluator', () => { expect(() => evaluate('new Fn()', { Fn })).toThrow(/Cannot instantiate object .*/i); }); - it('Should not support invalid property references on supported objects', () => { + it('should not support invalid property references on supported objects', () => { expect(() => evaluate('new Date().__proto__')).toThrow(/Cannot access the __proto__ property .*/i); expect(() => evaluate('new Date().prototype')).toThrow(/Cannot access the prototype property .*/i); }); - it('Should not return invalid types', () => { + it('should not return invalid types', () => { expect(() => evaluate('a', { a: {} })).toThrow(/.* did not produce a valid result/i); expect(() => evaluateAsBoolean('a', { a: 'value' })).toThrow(/.* did not produce a valid result/i); expect(() => evaluateAsNumber('a', { a: true })).toThrow(/.* did not produce a valid result/i); @@ -149,16 +149,16 @@ describe('OpenMRS Expression Evaluator', () => { ); }); - it('Should support a compilation phase', () => { + it('should support a compilation phase', () => { const exp = compile('1 + 1'); expect(evaluate(exp)).toBe(2); }); - it('Should not support variable assignment', () => { + it('should not support variable assignment', () => { expect(() => evaluate('var a = 1; a')).toThrow(); }); - it('Should support asynchronous evaluation', async () => { + it('should support asynchronous evaluation', async () => { await expect(evaluateAsync('1 + 1')).resolves.toBe(2); let a = new Promise((resolve) => { setTimeout(() => resolve(1), 10); @@ -172,13 +172,13 @@ describe('OpenMRS Expression Evaluator', () => { ).resolves.toBe(2); }); - it('Should support mock functions', () => { + it('should support mock functions', () => { expect(evaluate('api.getValue()', { api: { getValue: jest.fn().mockImplementation(() => 'value') } })).toBe( 'value', ); }); - it('Should support real-world use-cases', () => { + it('should support real-world use-cases', () => { expect( evaluate('!isEmpty(array)', { array: [], @@ -199,7 +199,7 @@ describe('OpenMRS Expression Evaluator', () => { ).toBe(true); }); - it('Should throw an error with correct message for non-existent function', () => { + it('should throw an error with correct message for non-existent function', () => { expect(() => { evaluate('api.nonExistingFunction()', { api: {} }); }).toThrow('No function named nonExistingFunction is defined in this context'); @@ -216,7 +216,7 @@ describe('OpenMRS Expression Evaluator', () => { }).toThrow('No function named deepNested is defined in this context'); }); - it('Should throw an error with correct message for non-callable targets', () => { + it('should throw an error with correct message for non-callable targets', () => { expect(() => { evaluate('objectWrapper.path()', { objectWrapper: { diff --git a/packages/framework/esm-expression-evaluator/src/extractor.test.ts b/packages/framework/esm-expression-evaluator/src/extractor.test.ts index 860affaad..7d2f50c7f 100644 --- a/packages/framework/esm-expression-evaluator/src/extractor.test.ts +++ b/packages/framework/esm-expression-evaluator/src/extractor.test.ts @@ -2,46 +2,46 @@ import { describe, it, expect } from '@jest/globals'; import { extractVariableNames } from './extractor'; describe('OpenMRS Expression Extractor', () => { - it('Should return empty list for expression lacking variables', () => { + it('returns an empty list for expression lacking variables', () => { expect(extractVariableNames('1 + 1')).toEqual([]); }); - it('Should support basic variables', () => { + it('supports basic variables', () => { expect(extractVariableNames('1 + a')).toEqual(['a']); }); - it('Should extracting both variables from binary operators', () => { + it('extracts both variables from binary operators', () => { expect(extractVariableNames('a ?? b')).toEqual(['a', 'b']); }); - it('Should support functions', () => { + it('supports functions', () => { expect(extractVariableNames('a(b)')).toEqual(['a', 'b']); }); - it('Should support built-in functions', () => { + it('supports built-in functions', () => { expect(extractVariableNames('a.includes("v")')).toEqual(['a']); expect(extractVariableNames('"value".includes(a)')).toEqual(['a']); expect(extractVariableNames('(3.14159).toPrecision(a)')).toEqual(['a']); }); - it('Should support string templates', () => { + it('supports string templates', () => { expect(extractVariableNames('`${a.b}`')).toEqual(['a']); }); - it('Should support RegExp', () => { + it('supports RegExp', () => { expect(extractVariableNames('/.*/.test(a)')).toEqual(['a']); }); - it('Should support global objects', () => { + it('supports global objects', () => { expect(extractVariableNames('Math.min(a, b, c)')).toEqual(['a', 'b', 'c']); expect(extractVariableNames('isNaN(a)')).toEqual(['a']); }); - it('Should support arrow functions inside expressions', () => { + it('supports arrow functions inside expressions', () => { expect(extractVariableNames('[1, 2, 3].find(v => v === a)')).toEqual(['a']); }); - it('Should support real-world use-cases', () => { + it('supports real-world use-cases', () => { expect(extractVariableNames('!isEmpty(array)')).toEqual(['isEmpty', 'array']); expect( diff --git a/packages/framework/esm-framework/src/integration-tests/extension-config.test.tsx b/packages/framework/esm-framework/src/integration-tests/extension-config.test.tsx index 9f14e7aa1..6f65d779a 100644 --- a/packages/framework/esm-framework/src/integration-tests/extension-config.test.tsx +++ b/packages/framework/esm-framework/src/integration-tests/extension-config.test.tsx @@ -506,6 +506,7 @@ async function registerSimpleExtension( ); }; + registerExtension({ name, moduleName, diff --git a/packages/framework/esm-navigation/src/navigation/interpolate-string.test.ts b/packages/framework/esm-navigation/src/navigation/interpolate-string.test.ts index 89a72cf30..37dff076a 100644 --- a/packages/framework/esm-navigation/src/navigation/interpolate-string.test.ts +++ b/packages/framework/esm-navigation/src/navigation/interpolate-string.test.ts @@ -32,4 +32,29 @@ describe('interpolateString', () => { const result = interpolateString('test ok', { one: '1', two: '2' }); expect(result).toBe('test ok'); }); + + it('handles multiple occurrences of the same parameter', () => { + const result = interpolateString('${value} and ${value}', { value: 'test' }); + expect(result).toBe('test and test'); + }); + + it('handles empty string parameters', () => { + const result = interpolateString('prefix${param}suffix', { param: '' }); + expect(result).toBe('prefixsuffix'); + }); + + it('leaves unreplaced parameters in template string', () => { + const result = interpolateString('${exists} ${missing}', { exists: 'value' }); + expect(result).toBe('value ${missing}'); + }); + + it('removes double slashes at the start of URLs', () => { + const result = interpolateUrl('${openmrsBase}/${path}', { path: 'test' }); + expect(result).toBe('/openmrs/test'); + }); + + it('handles special characters in parameters', () => { + const result = interpolateString('${param}', { param: '${}' }); + expect(result).toBe('${}'); + }); }); diff --git a/packages/framework/esm-react-utils/src/ConfigurableLink.test.tsx b/packages/framework/esm-react-utils/src/ConfigurableLink.test.tsx index 790c3f48a..ab62ac9bb 100644 --- a/packages/framework/esm-react-utils/src/ConfigurableLink.test.tsx +++ b/packages/framework/esm-react-utils/src/ConfigurableLink.test.tsx @@ -7,7 +7,7 @@ import { ConfigurableLink } from './ConfigurableLink'; jest.mock('single-spa'); -const mockNavigate = navigate as jest.Mock; +const mockNavigate = jest.mocked(navigate); describe(`ConfigurableLink`, () => { const path = '${openmrsSpaBase}/home'; @@ -15,7 +15,7 @@ describe(`ConfigurableLink`, () => { mockNavigate.mockClear(); }); - it(`interpolates the link`, async () => { + it('interpolates the link', async () => { render( SPA Home @@ -29,7 +29,7 @@ describe(`ConfigurableLink`, () => { expect(link.closest('a')).toHaveAttribute('href', '/openmrs/spa/home'); }); - it(`calls navigate on normal click but not special clicks`, async () => { + it('calls navigate on normal click but not special clicks', async () => { render( SPA Home @@ -44,7 +44,7 @@ describe(`ConfigurableLink`, () => { expect(navigate).toHaveBeenCalledWith({ to: path }); }); - it(`calls navigate on enter`, async () => { + it('calls navigate on enter', async () => { render( SPA Home diff --git a/packages/framework/esm-react-utils/src/extensions.test.tsx b/packages/framework/esm-react-utils/src/extensions.test.tsx index 29f58c558..1916000b5 100644 --- a/packages/framework/esm-react-utils/src/extensions.test.tsx +++ b/packages/framework/esm-react-utils/src/extensions.test.tsx @@ -22,7 +22,7 @@ import { // For some reason in the test context `isEqual` always returns true // when using the import substitution in jest.config.js. Here's a custom // mock. -jest.mock('lodash-es/isEqual', () => (a, b) => JSON.stringify(a) == JSON.stringify(b)); +jest.mock('lodash-es/isEqual', () => (a, b) => JSON.stringify(a) === JSON.stringify(b)); describe('ExtensionSlot, Extension, and useExtensionSlotMeta', () => { beforeEach(() => { diff --git a/packages/framework/esm-react-utils/src/useConfig.test.tsx b/packages/framework/esm-react-utils/src/useConfig.test.tsx index 65ac0cbec..9365b60e6 100644 --- a/packages/framework/esm-react-utils/src/useConfig.test.tsx +++ b/packages/framework/esm-react-utils/src/useConfig.test.tsx @@ -34,7 +34,7 @@ function clearConfig() { describe(`useConfig in root context`, () => { afterEach(clearConfig); - it(`can return config as a react hook`, async () => { + it('can return config as a react hook', async () => { defineConfigSchema('foo-module', { thing: { _default: 'The first thing', @@ -52,7 +52,7 @@ describe(`useConfig in root context`, () => { await waitFor(() => expect(screen.findByText('The first thing')).toBeTruthy()); }); - it(`can handle multiple calls to useConfig from different modules`, async () => { + it('can handle multiple calls to useConfig from different modules', async () => { defineConfigSchema('foo-module', { thing: { _default: 'foo thing', @@ -116,7 +116,7 @@ describe(`useConfig in root context`, () => { describe(`useConfig in an extension`, () => { afterEach(clearConfig); - it(`can return extension config as a react hook`, async () => { + it('can return extension config as a react hook', async () => { defineConfigSchema('ext-module', { thing: { _default: 'The basics', @@ -144,7 +144,7 @@ describe(`useConfig in an extension`, () => { await waitFor(() => expect(screen.findByText('The basics')).toBeTruthy()); }); - it(`can handle multiple extensions`, async () => { + it('can handle multiple extensions', async () => { defineConfigSchema('first-module', { thing: { _default: 'first thing', diff --git a/packages/framework/esm-react-utils/src/useOnClickOutside.test.tsx b/packages/framework/esm-react-utils/src/useOnClickOutside.test.tsx index ae7eb2c16..061331180 100644 --- a/packages/framework/esm-react-utils/src/useOnClickOutside.test.tsx +++ b/packages/framework/esm-react-utils/src/useOnClickOutside.test.tsx @@ -5,7 +5,6 @@ import { useOnClickOutside } from './useOnClickOutside'; describe('useOnClickOutside', () => { const handler: (e: Event) => void = jest.fn(); - afterEach(() => (handler as jest.Mock).mockClear()); it('should call the handler when clicking outside', async () => { const user = userEvent.setup(); diff --git a/packages/framework/esm-styleguide/src/custom-overflow-menu/custom-overflow-menu.test.tsx b/packages/framework/esm-styleguide/src/custom-overflow-menu/custom-overflow-menu.test.tsx index c12b3395e..a0e601e71 100644 --- a/packages/framework/esm-styleguide/src/custom-overflow-menu/custom-overflow-menu.test.tsx +++ b/packages/framework/esm-styleguide/src/custom-overflow-menu/custom-overflow-menu.test.tsx @@ -2,6 +2,9 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; import { render, screen } from '@testing-library/react'; import { CustomOverflowMenu } from './custom-overflow-menu.component'; +import { useLayoutType } from '@openmrs/esm-react-utils'; + +const mockUseLayoutType = jest.mocked(useLayoutType); describe('CustomOverflowMenuComponent', () => { it('should render', () => { @@ -27,4 +30,26 @@ describe('CustomOverflowMenuComponent', () => { await user.click(triggerButton); expect(triggerButton).toHaveAttribute('aria-expanded', 'false'); }); + + it('should apply deceased styling when deceased prop is true', () => { + render( + +
  • Option 1
  • + , + ); + + expect(screen.getByRole('button')).toHaveClass('deceased'); + }); + + it('should apply tablet-specific styling when on tablet layout', () => { + mockUseLayoutType.mockReturnValue('tablet'); + + render( + +
  • Option 1
  • +
    , + ); + + expect(screen.getByRole('list')).toHaveClass('cds--overflow-menu-options--lg'); + }); }); diff --git a/packages/framework/esm-styleguide/src/error-state/error-state.test.tsx b/packages/framework/esm-styleguide/src/error-state/error-state.test.tsx index 7466e6112..2c89560c7 100644 --- a/packages/framework/esm-styleguide/src/error-state/error-state.test.tsx +++ b/packages/framework/esm-styleguide/src/error-state/error-state.test.tsx @@ -1,13 +1,11 @@ -/** - * @jest-environment jsdom - * @jest-environment-options {"url": "https://jestjs.io/"} - */ - import React from 'react'; import { render, screen } from '@testing-library/react'; +import { useLayoutType } from '@openmrs/esm-react-utils'; import { ErrorState } from '.'; -describe('ErrorState: ', () => { +const mockUseLayoutType = jest.mocked(useLayoutType); + +describe('ErrorState', () => { it('renders an error state widget card', () => { const testError = { response: { @@ -25,4 +23,26 @@ describe('ErrorState: ', () => { ), ).toBeInTheDocument(); }); + + it('should render tablet layout when layout type is tablet', () => { + mockUseLayoutType.mockReturnValue('tablet'); + + render(); + // eslint-disable-next-line testing-library/no-node-access + expect(screen.getByRole('heading').parentElement).toHaveClass('tabletHeading'); + }); + + it('should render desktop layout when layout type is not tablet', () => { + mockUseLayoutType.mockReturnValue('small-desktop'); + + render(); + // eslint-disable-next-line testing-library/no-node-access + expect(screen.getByRole('heading').parentElement).toHaveClass('desktopHeading'); + }); + + it('should handle error with partial response data', () => { + const error = { response: { status: 404 } }; + render(); + expect(screen.getByText(/Error 404:/i)).toBeInTheDocument(); + }); }); diff --git a/packages/framework/esm-styleguide/src/location-picker/location-picker.test.tsx b/packages/framework/esm-styleguide/src/location-picker/location-picker.test.tsx index 6c742bbd8..4777d1c44 100644 --- a/packages/framework/esm-styleguide/src/location-picker/location-picker.test.tsx +++ b/packages/framework/esm-styleguide/src/location-picker/location-picker.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { openmrsFetch, useConfig, useSession } from '@openmrs/esm-framework'; +import { useConfig, useSession, type LoggedInUser, type Session } from '@openmrs/esm-framework'; import { inpatientWardResponse, locationResponseForNonExistingSearch, @@ -16,18 +16,17 @@ const validLocationUuid = '1ce1b7d4-c865-4178-82b0-5932e51503d6'; const inpatientWardLocationUuid = 'ba685651-ed3b-4e63-9b35-78893060758a'; const outpatientClinicLocationUuid = '44c3efb0-2583-4c80-a79e-1f756a03c0a1'; -const mockedOpenmrsFetch = openmrsFetch as jest.Mock; -const mockedUseConfig = useConfig as jest.Mock; -const mockUseSession = useSession as jest.Mock; +const mockUseConfig = jest.mocked(useConfig); +const mockUseSession = jest.mocked(useSession); -mockedUseConfig.mockReturnValue(mockConfig); +mockUseConfig.mockReturnValue(mockConfig); mockUseSession.mockReturnValue({ user: { display: 'Testy McTesterface', uuid: '90bd24b3-e700-46b0-a5ef-c85afdfededd', userProperties: {}, - }, -}); + } as LoggedInUser, +} as Session); jest.mock('@openmrs/esm-framework', () => ({ ...jest.requireActual('@openmrs/esm-framework'), @@ -50,10 +49,6 @@ jest.mock('@openmrs/esm-framework', () => ({ })); describe('LocationPicker', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - it('renders a list of login locations', async () => { await act(async () => { render(); diff --git a/packages/framework/esm-styleguide/src/page-header/page-header.test.tsx b/packages/framework/esm-styleguide/src/page-header/page-header.test.tsx index 8035f9380..7f7278d67 100644 --- a/packages/framework/esm-styleguide/src/page-header/page-header.test.tsx +++ b/packages/framework/esm-styleguide/src/page-header/page-header.test.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { getConfig } from '@openmrs/esm-config'; -import { PageHeaderContent } from './page-header.component'; import { getCoreTranslation } from '@openmrs/esm-translations'; +import { PageHeaderContent } from './page-header.component'; -const mockedGetConfig = jest.mocked(getConfig); -const mockedGetCoreTranslation = jest.mocked(getCoreTranslation); +const mockGetConfig = jest.mocked(getConfig); +const mockGetCoreTranslation = jest.mocked(getCoreTranslation); jest.mock('@openmrs/esm-config', () => ({ getConfig: jest.fn(), @@ -15,7 +15,7 @@ describe('PageHeaderContent', () => { const mockIllustration = ; it('renders title and illustration', async () => { - mockedGetConfig.mockResolvedValue({}); + mockGetConfig.mockResolvedValue({}); render(); @@ -24,8 +24,8 @@ describe('PageHeaderContent', () => { }); it('renders implementation name when provided in config', async () => { - mockedGetCoreTranslation.mockReturnValueOnce('Test Clinic'); - mockedGetConfig.mockResolvedValue({ implementationName: 'Test Clinic' }); + mockGetCoreTranslation.mockReturnValueOnce('Test Clinic'); + mockGetConfig.mockResolvedValue({ implementationName: 'Test Clinic' }); render(); @@ -33,7 +33,7 @@ describe('PageHeaderContent', () => { }); it('does not render implementation name when not provided in config', async () => { - mockedGetConfig.mockResolvedValue({}); + mockGetConfig.mockResolvedValue({}); render(); @@ -42,7 +42,7 @@ describe('PageHeaderContent', () => { }); it('applies custom className when provided', async () => { - mockedGetConfig.mockResolvedValue({}); + mockGetConfig.mockResolvedValue({}); const { container } = render( , @@ -54,7 +54,7 @@ describe('PageHeaderContent', () => { }); it('calls getConfig with correct module name', async () => { - mockedGetConfig.mockResolvedValue({}); + mockGetConfig.mockResolvedValue({}); render(); diff --git a/packages/framework/esm-styleguide/src/patient-banner/contact-details/patient-banner-contact-details.component.tsx b/packages/framework/esm-styleguide/src/patient-banner/contact-details/patient-banner-contact-details.component.tsx index 3b082bfe3..6d57f3478 100644 --- a/packages/framework/esm-styleguide/src/patient-banner/contact-details/patient-banner-contact-details.component.tsx +++ b/packages/framework/esm-styleguide/src/patient-banner/contact-details/patient-banner-contact-details.component.tsx @@ -30,7 +30,7 @@ const PatientLists: React.FC<{ patientUuid: string }> = ({ patientUuid }) => { {(() => { if (cohorts?.length > 0) { const sortedLists = cohorts.sort( - (a, b) => parseDate(a.startDate).getTime() - parseDate(b.startDate).getTime(), + (a, b) => parseDate(a?.startDate).getTime() - parseDate(b?.startDate).getTime(), ); const slicedLists = sortedLists.slice(0, 3); return slicedLists?.map((cohort) => ( diff --git a/packages/framework/esm-styleguide/src/patient-banner/contact-details/patient-banner-contact-details.test.tsx b/packages/framework/esm-styleguide/src/patient-banner/contact-details/patient-banner-contact-details.test.tsx index 02fd6d69f..5e037b9c9 100644 --- a/packages/framework/esm-styleguide/src/patient-banner/contact-details/patient-banner-contact-details.test.tsx +++ b/packages/framework/esm-styleguide/src/patient-banner/contact-details/patient-banner-contact-details.test.tsx @@ -8,11 +8,11 @@ import { useRelationships } from './useRelationships'; import { PatientBannerContactDetails } from './patient-banner-contact-details.component'; import { renderWithSwr } from '../../test-utils'; -const mockedUsePatient = usePatient as jest.Mock; -const mockedUsePatientAttributes = usePatientAttributes as jest.Mock; -const mockedUsePatientContactAttributes = usePatientContactAttributes as jest.Mock; -const mockUsePatientListsForPatient = usePatientListsForPatient as jest.Mock; -const mockUseRelationships = useRelationships as jest.Mock; +const mockUsePatient = jest.mocked(usePatient); +const mockUsePatientAttributes = jest.mocked(usePatientAttributes); +const mockUsePatientContactAttributes = jest.mocked(usePatientContactAttributes); +const mockUsePatientListsForPatient = jest.mocked(usePatientListsForPatient); +const mockUseRelationships = jest.mocked(useRelationships); const mockRelationships = [ { @@ -88,7 +88,7 @@ jest.mock('./useRelationships', () => ({ describe('ContactDetails', () => { beforeEach(() => { - mockedUsePatient.mockReturnValue({ + mockUsePatient.mockReturnValue({ isLoading: false, patient: { address: [ @@ -103,8 +103,11 @@ describe('ContactDetails', () => { ], telecom: [{ system: 'Cellular', value: '+0123456789' }], }, + patientUuid: '123e4567-e89b-12d3-a456-426614174000', + error: null, }); - mockedUsePatientContactAttributes.mockReturnValue({ + + mockUsePatientContactAttributes.mockReturnValue({ isLoading: false, contactAttributes: mockPersonAttributes, }); @@ -117,6 +120,8 @@ describe('ContactDetails', () => { mockUseRelationships.mockReturnValue({ isLoading: false, data: mockRelationships, + error: undefined, + isValidating: false, }); }); @@ -139,31 +144,40 @@ describe('ContactDetails', () => { it('patient related name should be a link', async () => { renderWithSwr(); - const relationShip = screen.getByRole('link', { name: /Amanda Robinson/ }); - expect(relationShip).toHaveAttribute('href', `/spa/patient/${mockRelationships[0].relativeUuid}/chart`); + + const relationship = screen.getByRole('link', { name: /Amanda Robinson/ }); + expect(relationship).toHaveAttribute('href', `/spa/patient/${mockRelationships[0].relativeUuid}/chart`); }); it('renders an empty state view when contact details, relations, patient lists and addresses are not available', async () => { - mockedUsePatient.mockReturnValue({ + mockUsePatient.mockReturnValue({ isLoading: false, - address: [], + patient: undefined, + patientUuid: null, + error: null, }); - mockedUsePatientAttributes.mockReturnValue({ + + mockUsePatientAttributes.mockReturnValue({ isLoading: false, attributes: [], error: null, }); - mockedUsePatientContactAttributes.mockReturnValue({ + + mockUsePatientContactAttributes.mockReturnValue({ isLoading: false, contactAttributes: [], }); + mockUsePatientListsForPatient.mockReturnValue({ isLoading: false, cohorts: [], }); + mockUseRelationships.mockReturnValue({ isLoading: false, data: [], + error: undefined, + isValidating: false, }); renderWithSwr(); diff --git a/packages/framework/esm-styleguide/src/patient-banner/contact-details/types.ts b/packages/framework/esm-styleguide/src/patient-banner/contact-details/types.ts index 76dbbeaf8..87b926977 100644 --- a/packages/framework/esm-styleguide/src/patient-banner/contact-details/types.ts +++ b/packages/framework/esm-styleguide/src/patient-banner/contact-details/types.ts @@ -50,5 +50,5 @@ interface Cohort { uuid: string; name: string; startDate: string; - endDate: string; + endDate: string | null; } diff --git a/packages/framework/esm-styleguide/src/patient-banner/contact-details/useRelationships.ts b/packages/framework/esm-styleguide/src/patient-banner/contact-details/useRelationships.ts index e1082ce4a..6ef7aa19a 100644 --- a/packages/framework/esm-styleguide/src/patient-banner/contact-details/useRelationships.ts +++ b/packages/framework/esm-styleguide/src/patient-banner/contact-details/useRelationships.ts @@ -18,7 +18,7 @@ export function useRelationships(patientUuid: string) { return { data: data ? formattedRelationships : null, - isError: error, + error, isLoading, isValidating, }; diff --git a/packages/framework/esm-styleguide/src/patient-photo/patient-photo.test.tsx b/packages/framework/esm-styleguide/src/patient-photo/patient-photo.test.tsx index e1bcf8ce5..c05bab6de 100644 --- a/packages/framework/esm-styleguide/src/patient-photo/patient-photo.test.tsx +++ b/packages/framework/esm-styleguide/src/patient-photo/patient-photo.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react'; import { PatientPhoto } from './patient-photo.component'; import { usePatientPhoto } from './usePatientPhoto'; -const mockedUsePatientPhoto = jest.mocked(usePatientPhoto); +const mockUsePatientPhoto = jest.mocked(usePatientPhoto); jest.mock('./usePatientPhoto', () => ({ usePatientPhoto: jest.fn(), @@ -20,7 +20,7 @@ const patientName = 'Freddy Mercury'; describe('PatientPhoto', () => { it('renders a progressbar when the patient photo is loading', () => { - mockedUsePatientPhoto.mockReturnValue({ + mockUsePatientPhoto.mockReturnValue({ isLoading: true, data: null, error: undefined, @@ -32,7 +32,7 @@ describe('PatientPhoto', () => { }); it('renders a placeholder image if the patient photo fails to load', async () => { - mockedUsePatientPhoto.mockReturnValue({ + mockUsePatientPhoto.mockReturnValue({ isLoading: false, data: { imageSrc: 'invalid-url.jpg', dateTime: '2024-01-01' }, error: undefined, @@ -66,7 +66,7 @@ describe('PatientPhoto', () => { }; window.Image = mockImage as any; - mockedUsePatientPhoto.mockReturnValue({ + mockUsePatientPhoto.mockReturnValue({ isLoading: false, data: { imageSrc: 'valid-image.jpg', dateTime: '2024-01-01' }, error: undefined, diff --git a/packages/framework/esm-styleguide/src/snackbars/snackbar.test.tsx b/packages/framework/esm-styleguide/src/snackbars/snackbar.test.tsx index 8f51ce514..9d49bfd9c 100644 --- a/packages/framework/esm-styleguide/src/snackbars/snackbar.test.tsx +++ b/packages/framework/esm-styleguide/src/snackbars/snackbar.test.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { act, render, screen, waitFor } from '@testing-library/react'; import { Snackbar } from './snackbar.component'; -import userEvent from '@testing-library/user-event'; jest.useFakeTimers(); -const mockedCloseSnackbar = jest.fn(); + +const mockCloseSnackbar = jest.fn(); describe('Snackbar component', () => { it('renders a snackbar notification', () => { @@ -46,7 +46,7 @@ describe('Snackbar component', () => { expect(snackbar).toBeInTheDocument(); act(() => jest.advanceTimersByTime(5000)); - await waitFor(() => expect(mockedCloseSnackbar).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(mockCloseSnackbar).toHaveBeenCalledTimes(1)); }); it('renders an actionable variant of the snackbar if actionButtonLabel is provided', () => { @@ -71,7 +71,7 @@ function renderSnackbar(overrides = {}) { autoClose: false, title: 'Order submitted', }, - closeSnackbar: mockedCloseSnackbar, + closeSnackbar: mockCloseSnackbar, }; render(); diff --git a/packages/framework/esm-styleguide/src/workspaces/action-menu-button/action-menu-button.test.tsx b/packages/framework/esm-styleguide/src/workspaces/action-menu-button/action-menu-button.test.tsx index a29294568..756ce96d8 100644 --- a/packages/framework/esm-styleguide/src/workspaces/action-menu-button/action-menu-button.test.tsx +++ b/packages/framework/esm-styleguide/src/workspaces/action-menu-button/action-menu-button.test.tsx @@ -1,15 +1,16 @@ import React from 'react'; import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { useLayoutType } from '@openmrs/esm-react-utils'; import { Pen } from '@carbon/react/icons'; +import { useLayoutType } from '@openmrs/esm-react-utils'; import { ActionMenuButton } from './action-menu-button.component'; -import { useWorkspaces } from '../workspaces'; +import { type OpenWorkspace, useWorkspaces, type WorkspacesInfo } from '../workspaces'; -const mockedUseLayoutType = useLayoutType as jest.Mock; +const mockUseLayoutType = jest.mocked(useLayoutType); +const mockUseWorkspaces = jest.mocked(useWorkspaces); jest.mock('@carbon/react/icons', () => ({ - ...(jest.requireActual('@carbon/react/icons') as jest.Mock), + ...jest.requireActual('@carbon/react/icons'), Pen: jest.fn(({ size }) =>
    size: {size}
    ), })); @@ -35,12 +36,12 @@ describe('ActionMenuButton', () => { it('should display tablet view', async () => { const user = userEvent.setup(); - (useWorkspaces as jest.Mock).mockReturnValue({ - workspaces: [{ type: 'order' }], + mockUseWorkspaces.mockReturnValue({ + workspaces: [{ type: 'order' } as unknown as OpenWorkspace], workspaceWindowState: 'normal', - }); + } as unknown as WorkspacesInfo); - mockedUseLayoutType.mockReturnValue('tablet'); + mockUseLayoutType.mockReturnValue('tablet'); const handler = jest.fn(); @@ -59,19 +60,19 @@ describe('ActionMenuButton', () => { const button = screen.getByRole('button', { name: /Visit note/i }); expect(button).toBeInTheDocument(); - await user.click(button); - expect(handler).toBeCalled(); + await user.click(button); + expect(handler).toHaveBeenCalled(); expect(button).not.toHaveClass('active'); }); it('should have not active className if workspace is not active', async () => { - (useWorkspaces as jest.Mock).mockReturnValue({ - workspaces: [{ type: 'order' }, { type: 'visit-note' }], + mockUseWorkspaces.mockReturnValue({ + workspaces: [{ type: 'order' }, { type: 'visit-note' } as unknown as OpenWorkspace], workspaceWindowState: 'normal', - }); + } as unknown as WorkspacesInfo); - mockedUseLayoutType.mockReturnValue('tablet'); + mockUseLayoutType.mockReturnValue('tablet'); const handler = jest.fn(); @@ -92,12 +93,12 @@ describe('ActionMenuButton', () => { }); it('should have active className if workspace is active', async () => { - (useWorkspaces as jest.Mock).mockReturnValue({ - workspaces: [{ type: 'visit-note' }, { type: 'order' }], + mockUseWorkspaces.mockReturnValue({ + workspaces: [{ type: 'visit-note' }, { type: 'order' } as unknown as OpenWorkspace], workspaceWindowState: 'normal', - }); + } as unknown as WorkspacesInfo); - mockedUseLayoutType.mockReturnValue('tablet'); + mockUseLayoutType.mockReturnValue('tablet'); const handler = jest.fn(); @@ -118,12 +119,12 @@ describe('ActionMenuButton', () => { }); it('should not display active className if workspace is active but workspace window is hidden', async () => { - (useWorkspaces as jest.Mock).mockReturnValue({ - workspaces: [{ type: 'visit-note' }, { type: 'order' }], + mockUseWorkspaces.mockReturnValue({ + workspaces: [{ type: 'visit-note' }, { type: 'order' } as unknown as OpenWorkspace], workspaceWindowState: 'hidden', - }); + } as unknown as WorkspacesInfo); - mockedUseLayoutType.mockReturnValue('tablet'); + mockUseLayoutType.mockReturnValue('tablet'); const handler = jest.fn(); @@ -146,12 +147,12 @@ describe('ActionMenuButton', () => { it('should display desktop view', async () => { const user = userEvent.setup(); - (useWorkspaces as jest.Mock).mockReturnValue({ - workspaces: [{ type: 'order' }], + mockUseWorkspaces.mockReturnValue({ + workspaces: [{ type: 'order' } as unknown as OpenWorkspace], workspaceWindowState: 'normal', - }); + } as unknown as WorkspacesInfo); - mockedUseLayoutType.mockReturnValue('small-desktop'); + mockUseLayoutType.mockReturnValue('small-desktop'); const handler = jest.fn(); @@ -171,18 +172,18 @@ describe('ActionMenuButton', () => { const button = screen.getByRole('button'); expect(button).toBeInTheDocument(); await user.click(button); - expect(handler).toBeCalled(); + expect(handler).toHaveBeenCalled(); expect(button).not.toHaveClass('active'); }); it('should display have active className if workspace is not active', async () => { - (useWorkspaces as jest.Mock).mockReturnValue({ - workspaces: [{ type: 'order' }, { type: 'visit-note' }], + mockUseWorkspaces.mockReturnValue({ + workspaces: [{ type: 'order' }, { type: 'visit-note' } as unknown as OpenWorkspace], workspaceWindowState: 'normal', - }); + } as unknown as WorkspacesInfo); - mockedUseLayoutType.mockReturnValue('small-desktop'); + mockUseLayoutType.mockReturnValue('small-desktop'); const handler = jest.fn(); @@ -203,12 +204,12 @@ describe('ActionMenuButton', () => { }); it('should display active className if workspace is active and workspace window is normal', async () => { - (useWorkspaces as jest.Mock).mockReturnValue({ - workspaces: [{ type: 'visit-note' }, { type: 'order' }], + mockUseWorkspaces.mockReturnValue({ + workspaces: [{ type: 'visit-note' }, { type: 'order' } as unknown as OpenWorkspace], workspaceWindowState: 'normal', - }); + } as unknown as WorkspacesInfo); - mockedUseLayoutType.mockReturnValue('small-desktop'); + mockUseLayoutType.mockReturnValue('small-desktop'); const handler = jest.fn(); @@ -229,12 +230,12 @@ describe('ActionMenuButton', () => { }); it('should not display active className if workspace is active but workspace window is hidden', async () => { - (useWorkspaces as jest.Mock).mockReturnValue({ - workspaces: [{ type: 'visit-note' }, { type: 'order' }], + mockUseWorkspaces.mockReturnValue({ + workspaces: [{ type: 'visit-note' }, { type: 'order' } as unknown as OpenWorkspace], workspaceWindowState: 'hidden', - }); + } as unknown as WorkspacesInfo); - mockedUseLayoutType.mockReturnValue('small-desktop'); + mockUseLayoutType.mockReturnValue('small-desktop'); const handler = jest.fn(); diff --git a/packages/framework/esm-styleguide/src/workspaces/container/workspace-container.test.tsx b/packages/framework/esm-styleguide/src/workspaces/container/workspace-container.test.tsx index 581a14108..177c0d0f7 100644 --- a/packages/framework/esm-styleguide/src/workspaces/container/workspace-container.test.tsx +++ b/packages/framework/esm-styleguide/src/workspaces/container/workspace-container.test.tsx @@ -22,8 +22,8 @@ jest.mock('react-i18next', () => ({ useTranslation: jest.fn().mockImplementation(() => ({ t: (arg: string) => arg })), })); -const mockedIsDesktop = isDesktop as unknown as jest.Mock; -const mockedUseLayoutType = useLayoutType as jest.Mock; +const mockIsDesktop = jest.mocked(isDesktop); +const mockUseLayoutType = jest.mocked(useLayoutType); window.history.pushState({}, 'Workspace Container', '/workspace-container'); @@ -55,32 +55,40 @@ describe('WorkspaceContainer in window mode', () => { }); test('should translate the workspace title inside the workspace container', () => { - mockedIsDesktop.mockReturnValue(true); + mockIsDesktop.mockReturnValue(true); renderWorkspaceWindow(); act(() => launchWorkspace('clinical-form')); + let header = screen.getByRole('banner'); expect(within(header).getByText('clinicalForm')).toBeInTheDocument(); + act(() => launchWorkspace('order-basket')); + header = screen.getByRole('banner'); expect(within(header).getByText('orderBasket')).toBeInTheDocument(); }); test('should override title via additional props and via setTitle', async () => { - mockedIsDesktop.mockReturnValue(true); + mockIsDesktop.mockReturnValue(true); renderWorkspaceWindow(); // In this line we are also verifying that the type argument to `launchWorkspace` // behaves as expected, constraining the type of the `additionalProps` argument. + act(() => launchWorkspace('clinical-form', { workspaceTitle: 'COVID Admission', patientUuid: '123', }), ); + const header = screen.getByRole('banner'); expect(within(header).getByText('COVID Admission')).toBeInTheDocument(); + const utils = renderHook(() => useWorkspaces()); act(() => utils.result.current.workspaces[0].setTitle('COVID Discharge')); + expect(within(header).getByText('COVID Discharge')).toBeInTheDocument(); + act(() => utils.result.current.workspaces[0].setTitle('Space Ghost',
    Space Ghost
    ), ); @@ -88,18 +96,24 @@ describe('WorkspaceContainer in window mode', () => { }); test('re-launching workspace should update title, but only if setTitle was not used', async () => { - mockedIsDesktop.mockReturnValue(true); + mockIsDesktop.mockReturnValue(true); renderWorkspaceWindow(); + // In this line we are also testing that `launchWorkspace` allows arbitrary additional props // when no type argument is provided. + act(() => launchWorkspace('clinical-form', { workspaceTitle: 'COVID Admission', foo: 'bar' })); + const header = screen.getByRole('banner'); expect(within(header).getByText('COVID Admission')).toBeInTheDocument(); + act(() => launchWorkspace('clinical-form', { workspaceTitle: 'COVID Discharge' })); expect(within(header).getByText('COVID Discharge')).toBeInTheDocument(); + const utils = renderHook(() => useWorkspaces()); act(() => utils.result.current.workspaces[0].setTitle('Fancy Special Title')); expect(within(header).getByText('Fancy Special Title')).toBeInTheDocument(); + act(() => launchWorkspace('clinical-form', { workspaceTitle: 'COVID Admission Again' })); expect(within(header).getByText('Fancy Special Title')).toBeInTheDocument(); }); @@ -107,15 +121,20 @@ describe('WorkspaceContainer in window mode', () => { test('should reopen hidden workspace window when user relaunches the same workspace window', async () => { const user = userEvent.setup(); const utils = renderHook(() => useWorkspaces()); - mockedIsDesktop.mockReturnValue(true); + mockIsDesktop.mockReturnValue(true); + expect(utils.result.current.workspaces.length).toBe(0); + renderWorkspaceWindow(); act(() => launchWorkspace('clinical-form', { workspaceTitle: 'POC Triage' })); + expect(utils.result.current.workspaces.length).toBe(1); + const header = screen.getByRole('banner'); expect(within(header).getByText('POC Triage')).toBeInTheDocument(); expectToBeVisible(screen.getByRole('complementary')); + let input = screen.getByRole('textbox'); await user.type(input, "what's good"); @@ -124,6 +143,7 @@ describe('WorkspaceContainer in window mode', () => { expect(screen.queryByRole('complementary')).toHaveClass('hiddenRelative'); act(() => launchWorkspace('clinical-form', { workspaceTitle: 'POC Triage' })); + expectToBeVisible(await screen.findByRole('complementary')); expect(screen.queryByRole('complementary')).not.toHaveClass('hiddenRelative'); expect(screen.queryByRole('complementary')).not.toHaveClass('hiddenFixed'); @@ -134,10 +154,11 @@ describe('WorkspaceContainer in window mode', () => { test('should toggle between maximized and normal screen size', async () => { const user = userEvent.setup(); - mockedIsDesktop.mockReturnValue(true); + mockIsDesktop.mockReturnValue(true); renderWorkspaceWindow(); act(() => launchWorkspace('clinical-form')); + const header = screen.getByRole('banner'); expect(within(header).getByText('clinicalForm')).toBeInTheDocument(); // eslint-disable-next-line testing-library/no-node-access @@ -160,19 +181,25 @@ describe('WorkspaceContainer in window mode', () => { // Try this again periodically to see if it starts working. xtest("shouldn't lose data when transitioning between workspaces", async () => { renderWorkspaceWindow(); + const user = userEvent.setup(); act(() => launchWorkspace('clinical-form')); + let container = screen.getByRole('complementary'); expect(within(container).getByText('clinical-form')).toBeInTheDocument(); + let input = screen.getByRole('textbox'); - await user.type(input, 'howdy'); + await user.type(input, 'howdy'); await user.click(screen.getByRole('button', { name: 'Hide' })); + act(() => launchWorkspace('order-basket')); + container = screen.getByRole('complementary'); expect(within(container).getByText('order-basket')).toBeInTheDocument(); act(() => launchWorkspace('clinical-form')); + expect(within(container).getByText('clinical-form')).toBeInTheDocument(); input = screen.getByRole('textbox'); expect(input).toHaveValue('howdy'); @@ -198,8 +225,9 @@ describe('WorkspaceContainer in overlay mode', () => { }); it('opens with overridable title and closes', async () => { - mockedUseLayoutType.mockReturnValue('small-desktop'); + mockUseLayoutType.mockReturnValue('small-desktop'); const user = userEvent.setup(); + renderWorkspaceOverlay(); act(() => launchWorkspace('patient-search', { workspaceTitle: 'Make an appointment' })); diff --git a/packages/framework/esm-styleguide/src/workspaces/container/workspace-renderer.test.tsx b/packages/framework/esm-styleguide/src/workspaces/container/workspace-renderer.test.tsx index e8bf5a028..6a3c0f918 100644 --- a/packages/framework/esm-styleguide/src/workspaces/container/workspace-renderer.test.tsx +++ b/packages/framework/esm-styleguide/src/workspaces/container/workspace-renderer.test.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { WorkspaceRenderer } from './workspace-renderer.component'; import { getWorkspaceGroupStore } from '../workspaces'; -import Parcel from 'single-spa-react/parcel'; const mockFn = jest.fn(); @@ -15,11 +14,12 @@ jest.mock('single-spa-react/parcel', () => describe('WorkspaceRenderer', () => { it('should render workspace', async () => { - const mockedCloseWorkspace = jest.fn(); - const mockedCloseWorkspaceWithSavedChanges = jest.fn(); - const mockedPromptBeforeClosing = jest.fn(); - const mockedSetTitle = jest.fn(); - const mockedLoadFn = jest.fn().mockImplementation(() => Promise.resolve({ default: 'file-content' })); + const mockCloseWorkspace = jest.fn(); + const mockCloseWorkspaceWithSavedChanges = jest.fn(); + const mockPromptBeforeClosing = jest.fn(); + const mockSetTitle = jest.fn(); + const mockLoadFn = jest.fn().mockImplementation(() => Promise.resolve({ default: 'file-content' })); + getWorkspaceGroupStore('test-sidebar-store')?.setState({ // Testing that the workspace group state should be overrided by additionalProps foo: false, @@ -29,13 +29,13 @@ describe('WorkspaceRenderer', () => { { ); expect(screen.getByText('Loading ...')).toBeInTheDocument(); - expect(mockedLoadFn).toHaveBeenCalled(); + expect(mockLoadFn).toHaveBeenCalled(); + await screen.findByTestId('mocked-parcel'); expect(mockFn).toHaveBeenCalledWith({ config: 'file-content', mountParcel: undefined, - closeWorkspace: mockedCloseWorkspace, - closeWorkspaceWithSavedChanges: mockedCloseWorkspaceWithSavedChanges, - promptBeforeClosing: mockedPromptBeforeClosing, - setTitle: mockedSetTitle, + closeWorkspace: mockCloseWorkspace, + closeWorkspaceWithSavedChanges: mockCloseWorkspaceWithSavedChanges, + promptBeforeClosing: mockPromptBeforeClosing, + setTitle: mockSetTitle, foo: 'true', bar: 'true', }); diff --git a/packages/framework/esm-styleguide/src/workspaces/workspaces.test.ts b/packages/framework/esm-styleguide/src/workspaces/workspaces.test.ts index 7e05d3a5a..51052e473 100644 --- a/packages/framework/esm-styleguide/src/workspaces/workspaces.test.ts +++ b/packages/framework/esm-styleguide/src/workspaces/workspaces.test.ts @@ -1,3 +1,5 @@ +import { clearMockExtensionRegistry } from '@openmrs/esm-framework/mock'; +import { registerExtension, registerWorkspace } from '@openmrs/esm-extensions'; import { type Prompt, cancelPrompt, @@ -8,8 +10,6 @@ import { launchWorkspaceGroup, resetWorkspaceStore, } from './workspaces'; -import { registerExtension, registerWorkspace } from '@openmrs/esm-extensions'; -import { clearMockExtensionRegistry } from '@openmrs/esm-framework/mock'; describe('workspace system', () => { beforeEach(() => { @@ -20,12 +20,17 @@ describe('workspace system', () => { test('registering, launching, and closing a workspace', () => { const store = getWorkspaceStore(); registerWorkspace({ name: 'allergies', title: 'Allergies', load: jest.fn(), moduleName: '@openmrs/foo' }); + launchWorkspace('allergies', { foo: true }); + expect(store.getState().openWorkspaces.length).toEqual(1); + const allergies = store.getState().openWorkspaces[0]; expect(allergies.name).toBe('allergies'); expect(allergies.additionalProps['foo']).toBe(true); + allergies.closeWorkspace(); + expect(store.getState().openWorkspaces.length).toEqual(0); }); @@ -33,8 +38,11 @@ describe('workspace system', () => { it('should launch a workspace', () => { const store = getWorkspaceStore(); registerWorkspace({ name: 'allergies', title: 'Allergies', load: jest.fn(), moduleName: '@openmrs/foo' }); + launchWorkspace('allergies', { foo: true }); + expect(store.getState().openWorkspaces.length).toEqual(1); + const allergies = store.getState().openWorkspaces[0]; expect(allergies.name).toBe('allergies'); expect(allergies.additionalProps['foo']).toBe(true); @@ -42,6 +50,7 @@ describe('workspace system', () => { test('should update additionalProps when re-opening an already opened form with same name but with different props', () => { const store = getWorkspaceStore(); + registerWorkspace({ name: 'POC HIV Form', title: 'Clinical Form', load: jest.fn(), moduleName: '@openmrs/foo' }); launchWorkspace('POC HIV Form', { workspaceTitle: 'POC HIV Form' }); @@ -64,12 +73,17 @@ describe('workspace system', () => { it('should show a modal when a workspace is already open (with changes) and it cannot hide', () => { const store = getWorkspaceStore(); registerWorkspace({ name: 'allergies', title: 'Allergies', load: jest.fn(), moduleName: '@openmrs/foo' }); + launchWorkspace('allergies', { foo: true }); + expect(store.getState().openWorkspaces.length).toEqual(1); + const allergies = store.getState().openWorkspaces?.[0]; allergies.promptBeforeClosing(() => true); + registerWorkspace({ name: 'conditions', title: 'Conditions', load: jest.fn(), moduleName: '@openmrs/foo' }); launchWorkspace('conditions', { foo: true }); + const prompt = store.getState().prompt as Prompt; expect(prompt).toBeTruthy(); expect(prompt.title).toMatch(/unsaved changes/i); @@ -77,8 +91,11 @@ describe('workspace system', () => { 'There may be unsaved changes in Allergies. Please save them before opening another workspace.', ); expect(prompt.confirmText).toMatch(/Open anyway/i); + prompt.onConfirm(); + expect(store.getState().openWorkspaces.length).toEqual(1); + const openedWorkspace = store.getState().openWorkspaces[0]; expect(openedWorkspace.name).toBe('conditions'); expect(openedWorkspace.name).not.toBe('Allergies'); @@ -94,8 +111,11 @@ describe('workspace system', () => { type: 'allergies-form', moduleName: '@openmrs/foo', }); + launchWorkspace('allergies', { foo: true }); + expect(store.getState().openWorkspaces.length).toEqual(1); + registerWorkspace({ name: 'conditions', title: 'Conditions', @@ -104,7 +124,9 @@ describe('workspace system', () => { moduleName: '@openmrs/foo', }); launchWorkspace('conditions', { foo: true }); + expect(store.getState().openWorkspaces.length).toEqual(2); + const openedWorkspaces = store.getState().openWorkspaces; expect(openedWorkspaces[0].name).toBe('conditions'); expect(openedWorkspaces[1].name).not.toBe('Allergies'); @@ -112,6 +134,7 @@ describe('workspace system', () => { it("should show a modal when launching a workspace with type matching the already opened workspace's type and should show the previous opened workspace in front when showing the modal", () => { const store = getWorkspaceStore(); + registerWorkspace({ name: 'allergies', title: 'Allergies', @@ -135,17 +158,23 @@ describe('workspace system', () => { type: 'form', moduleName: '@openmrs/foo', }); + launchWorkspace('allergies'); launchWorkspace('conditions'); + expect(store.getState().openWorkspaces.length).toEqual(2); expect(store.getState().openWorkspaces[0].name).toBe('conditions'); expect(store.getState().openWorkspaces[1].name).toBe('allergies'); + const allergies = store.getState().openWorkspaces[1]; allergies.promptBeforeClosing(() => true); + launchWorkspace('vitals'); + expect(store.getState().openWorkspaces.length).toEqual(2); expect(store.getState().openWorkspaces[0].name).toBe('allergies'); expect(store.getState().openWorkspaces[1].name).toBe('conditions'); + const prompt = store.getState().prompt as Prompt; expect(prompt).toBeTruthy(); expect(prompt.title).toMatch(/unsaved changes/i); @@ -153,7 +182,9 @@ describe('workspace system', () => { 'There may be unsaved changes in Allergies. Please save them before opening another workspace.', ); expect(prompt.confirmText).toMatch(/Open anyway/i); + prompt.onConfirm(); + expect(store.getState().openWorkspaces.length).toEqual(2); expect(store.getState().openWorkspaces[0].name).toBe('vitals'); expect(store.getState().openWorkspaces[1].name).toBe('conditions'); @@ -161,6 +192,7 @@ describe('workspace system', () => { it("should not show a modal when launching a workspace with type not matching with any already opened workspace's type", () => { const store = getWorkspaceStore(); + registerWorkspace({ name: 'allergies', title: 'Allergies', @@ -184,16 +216,21 @@ describe('workspace system', () => { type: 'vitals-form', moduleName: '@openmrs/foo', }); + launchWorkspace('allergies'); launchWorkspace('conditions'); + expect(store.getState().openWorkspaces.length).toEqual(2); expect(store.getState().openWorkspaces[0].name).toBe('conditions'); expect(store.getState().openWorkspaces[1].name).toBe('allergies'); + launchWorkspace('vitals'); + expect(store.getState().openWorkspaces.length).toEqual(3); expect(store.getState().openWorkspaces[0].name).toBe('vitals'); expect(store.getState().openWorkspaces[1].name).toBe('conditions'); expect(store.getState().openWorkspaces[2].name).toBe('allergies'); + const prompt = store.getState().prompt; expect(prompt).toBeFalsy(); }); @@ -230,34 +267,45 @@ describe('workspace system', () => { type: 'form', moduleName: '@openmrs/foo', }); + launchWorkspace('allergies'); launchWorkspace('attachments'); launchWorkspace('conditions'); + expect(store.getState().openWorkspaces.length).toEqual(3); expect(store.getState().openWorkspaces.map((w) => w.name)).toEqual(['conditions', 'attachments', 'allergies']); + const conditionsWorkspace = store.getState().openWorkspaces?.[0]; const allergiesWorkspace = store.getState().openWorkspaces?.[2]; conditionsWorkspace.promptBeforeClosing(() => true); allergiesWorkspace.promptBeforeClosing(() => true); launchWorkspace('vitals'); + expect(store.getState().openWorkspaces.length).toEqual(3); expect(store.getState().openWorkspaces[0].name).toBe('conditions'); + const prompt = store.getState().prompt as Prompt; expect(prompt).toBeTruthy(); expect(prompt.body).toMatch( 'There may be unsaved changes in Conditions. Please save them before opening another workspace.', ); // Closing the conditions workspace because it cannot be hidden + prompt.onConfirm(); + expect(store.getState().openWorkspaces.length).toEqual(2); // Bringing the `allergies` workspace in front because it matches the vitals workspace type expect(store.getState().openWorkspaces[0].name).toEqual('allergies'); + const prompt2 = store.getState().prompt as Prompt; + expect(prompt2).toBeTruthy(); expect(prompt2.body).toMatch( 'There may be unsaved changes in Allergies. Please save them before opening another workspace.', ); + prompt2.onConfirm(); + expect(store.getState().openWorkspaces.length).toEqual(2); expect(store.getState().openWorkspaces[0].name).toBe('vitals'); expect(store.getState().openWorkspaces[1].name).toBe('attachments'); @@ -290,19 +338,28 @@ describe('workspace system', () => { preferredWindowSize: 'maximized', moduleName: '@openmrs/foo', }); + launchWorkspace('allergies'); + expect(store.getState().openWorkspaces.length).toBe(1); expect(store.getState().workspaceWindowState).toBe('maximized'); + launchWorkspace('attachments'); + expect(store.getState().openWorkspaces.length).toBe(2); expect(store.getState().workspaceWindowState).toBe('normal'); + launchWorkspace('conditions'); + expect(store.getState().openWorkspaces.length).toBe(3); expect(store.getState().workspaceWindowState).toBe('maximized'); + store.getState().openWorkspaces[0].closeWorkspace({ ignoreChanges: true }); expect(store.getState().workspaceWindowState).toBe('normal'); + store.getState().openWorkspaces[0].closeWorkspace(); expect(store.getState().workspaceWindowState).toBe('maximized'); + store.getState().openWorkspaces[0].closeWorkspace({ ignoreChanges: true }); expect(store.getState().workspaceWindowState).toBe('normal'); }); @@ -334,50 +391,72 @@ describe('workspace system', () => { type: 'order', moduleName: '@openmrs/foo', }); + // Test opening the same workspace twice--should be a no-op launchWorkspace('conditions'); launchWorkspace('conditions'); + expect(store.getState().openWorkspaces.length).toEqual(1); + const conditionsWorkspace = store.getState().openWorkspaces[0]; conditionsWorkspace.promptBeforeClosing(() => true); + // Test opening a workspace of the same type--should require confirmation and then replace launchWorkspace('form-entry', { foo: true }); + expect(store.getState().openWorkspaces.length).toEqual(1); expect(store.getState().openWorkspaces[0].name).toBe('conditions'); + let prompt = store.getState().prompt as Prompt; expect(prompt.title).toMatch(/unsaved changes/i); + prompt.onConfirm(); + expect(store.getState().prompt).toBeNull(); expect(store.getState().openWorkspaces.length).toEqual(1); expect(store.getState().openWorkspaces[0].name).toBe('form-entry'); expect(store.getState().openWorkspaces[0].additionalProps['foo']).toBe(true); + // Test opening a workspace of a different type--should open directly launchWorkspace('order-meds'); + expect(store.getState().openWorkspaces.length).toEqual(2); expect(store.getState().openWorkspaces[0].name).toBe('order-meds'); expect(store.getState().openWorkspaces[1].name).toBe('form-entry'); + const formEntryWorkspace = store.getState().openWorkspaces[1]; formEntryWorkspace.promptBeforeClosing(() => true); + // Test going through confirmation flow while order-meds is open // Changing the form workspace shouldn't destroy the order-meds workspace launchWorkspace('conditions'); + expect(store.getState().openWorkspaces.length).toEqual(2); expect(store.getState().openWorkspaces[0].name).toBe('form-entry'); expect(store.getState().openWorkspaces[1].name).toBe('order-meds'); + prompt = store.getState().prompt as Prompt; + expect(prompt.title).toMatch(/unsaved changes/i); + cancelPrompt(); // should leave same workspaces intact + expect(store.getState().openWorkspaces.length).toEqual(2); expect(store.getState().openWorkspaces[0].name).toBe('form-entry'); expect(store.getState().openWorkspaces[1].name).toBe('order-meds'); expect(store.getState().prompt).toBeNull(); + launchWorkspace('conditions'); + prompt = store.getState().prompt as Prompt; prompt.onConfirm(); + expect(store.getState().openWorkspaces.length).toEqual(2); expect(store.getState().openWorkspaces[0].name).toBe('conditions'); expect(store.getState().openWorkspaces[1].name).toBe('order-meds'); + store.getState().openWorkspaces[0].closeWorkspace({ ignoreChanges: true }); + expect(store.getState().openWorkspaces.length).toEqual(1); expect(store.getState().openWorkspaces[0].name).toBe('order-meds'); }); @@ -386,17 +465,27 @@ describe('workspace system', () => { const store = getWorkspaceStore(); registerWorkspace({ name: 'hiv', title: 'HIV', load: jest.fn(), moduleName: '@openmrs/foo' }); registerWorkspace({ name: 'diabetes', title: 'Diabetes', load: jest.fn(), moduleName: '@openmrs/foo' }); + launchWorkspace('hiv'); + store.getState().openWorkspaces[0].promptBeforeClosing(() => false); + launchWorkspace('diabetes'); + expect(store.getState().prompt).toBeNull(); expect(store.getState().openWorkspaces[0].name).toBe('diabetes'); + store.getState().openWorkspaces[0].promptBeforeClosing(() => true); + launchWorkspace('hiv'); + expect(store.getState().openWorkspaces[0].name).toBe('diabetes'); + const prompt = store.getState().prompt as Prompt; expect(prompt.title).toMatch(/unsaved changes/i); + prompt.onConfirm(); + expect(store.getState().openWorkspaces[0].name).toBe('hiv'); }); @@ -409,8 +498,11 @@ describe('workspace system', () => { load: jest.fn(), meta: { title: 'Lab Results', screenSize: 'maximized' }, }); + launchWorkspace('lab-results', { foo: true }); + expect(store.getState().openWorkspaces.length).toEqual(1); + const workspace = store.getState().openWorkspaces[0]; expect(workspace.name).toEqual('lab-results'); expect(workspace.additionalProps['foo']).toBe(true); @@ -421,22 +513,28 @@ describe('workspace system', () => { test('launching unregistered workspace throws an error', () => { const store = getWorkspaceStore(); + expect(() => launchWorkspace('test-results')).toThrow(/test-results.*registered/i); }); test('respects promptBeforeClosing function before closing workspace, with unsaved changes', () => { const store = getWorkspaceStore(); registerWorkspace({ name: 'hiv', title: 'HIV', load: jest.fn(), moduleName: '@openmrs/foo' }); + launchWorkspace('hiv'); + store.getState().openWorkspaces[0].promptBeforeClosing(() => true); store.getState().openWorkspaces[0].closeWorkspace({ ignoreChanges: false }); + const prompt = store.getState().prompt as Prompt; expect(prompt.title).toMatch(/unsaved changes/i); expect(prompt.body).toBe( 'You may have unsaved changes in the opened workspace. Do you want to discard these changes?', ); expect(prompt.confirmText).toBe('Discard'); + prompt.onConfirm(); + expect(store.getState().prompt).toBeNull(); expect(store.getState().openWorkspaces.length).toBe(0); }); @@ -444,7 +542,9 @@ describe('workspace system', () => { describe('Testing `getWorkspaceGroupStore` function', () => { it('should return undefined if no workspace sidebar name is passed', () => { let workspaceGroupStore = getWorkspaceGroupStore(undefined); + expect(workspaceGroupStore).toBeUndefined(); + workspaceGroupStore = getWorkspaceGroupStore('default'); expect(workspaceGroupStore).toBeUndefined(); }); @@ -453,6 +553,7 @@ describe('workspace system', () => { const workspaceGroupStore = getWorkspaceGroupStore('ward-patient-store', { foo: true, }); + expect(workspaceGroupStore).toBeTruthy(); expect(workspaceGroupStore?.getState()?.['foo']).toBe(true); }); @@ -461,11 +562,14 @@ describe('workspace system', () => { let workspaceGroupStore = getWorkspaceGroupStore('ward-patient-store', { foo: true, }); + expect(workspaceGroupStore).toBeTruthy(); expect(workspaceGroupStore?.getState()?.['foo']).toBe(true); + workspaceGroupStore = getWorkspaceGroupStore('ward-patient-store', { bar: true, }); + expect(workspaceGroupStore).toBeTruthy(); expect(workspaceGroupStore?.getState()?.['foo']).toBe(true); expect(workspaceGroupStore?.getState()?.['bar']).toBe(true); @@ -488,17 +592,22 @@ describe('workspace system', () => { moduleName: '@openmrs/esm-ward-app', groups: ['ward-patient-store'], }); + launchWorkspaceGroup('ward-patient-store', { state: {}, workspaceToLaunch: { name: 'ward-patient-workspace' }, }); + const workspaceGroupStore = getWorkspaceGroupStore('ward-patient-store'); const workspaceStore = getWorkspaceStore(); expect(workspaceStore.getState().openWorkspaces.length).toBe(1); expect(workspaceStore.getState().openWorkspaces[0].name).toBe('ward-patient-workspace'); expect(workspaceGroupStore).toBeTruthy(); + workspaceStore.getState().openWorkspaces[0].closeWorkspace({ ignoreChanges: true }); + launchWorkspace('allergies'); + expect(workspaceStore.getState().openWorkspaces.length).toBe(1); expect(workspaceStore.getState().openWorkspaces[0].name).toBe('allergies'); expect(workspaceGroupStore?.getState()).toStrictEqual({}); @@ -519,10 +628,13 @@ describe('workspace system', () => { }, workspaceToLaunch: { name: 'ward-patient-workspace' }, }); + const workspaceGroupStore = getWorkspaceGroupStore('ward-patient-store'); expect(workspaceGroupStore).toBeTruthy(); expect(workspaceGroupStore?.getState()?.['foo']).toBe(true); + closeWorkspace('ward-patient-workspace'); + expect(workspaceGroupStore?.getState()?.['foo']).toBeUndefined(); expect(workspaceGroupStore?.getState()).toStrictEqual({}); }); @@ -545,21 +657,26 @@ describe('workspace system', () => { moduleName: '@openmrs/esm-ward-app', groups: [workspaceGroup], }); + launchWorkspaceGroup(workspaceGroup, { state: { foo: true, }, workspaceToLaunch: { name: 'ward-patient-workspace' }, }); + const workspaceStore = getWorkspaceStore(); const workspaceGroupStore = getWorkspaceGroupStore(workspaceGroup); expect(workspaceStore.getState().openWorkspaces.length).toBe(1); expect(workspaceGroupStore).toBeTruthy(); expect(workspaceGroupStore?.getState()?.['foo']).toBe(true); + launchWorkspace('transfer-patient-workspace', { bar: false, }); + expect(workspaceStore.getState().openWorkspaces.length).toBe(1); + const transferPatientWorkspace = workspaceStore.getState().openWorkspaces[0]; expect(workspaceGroupStore?.getState()?.['foo']).toBe(true); expect(workspaceGroupStore?.getState()?.['bar']).toBe(false); @@ -582,6 +699,7 @@ describe('workspace system', () => { moduleName: '@openmrs/esm-ward-app', groups: ['another-sidebar-group'], }); + const workspaceStore = getWorkspaceStore(); launchWorkspaceGroup('ward-patient-store', { state: { @@ -589,17 +707,21 @@ describe('workspace system', () => { }, workspaceToLaunch: { name: 'ward-patient-workspace' }, }); + const wardPatientGroupStore = getWorkspaceGroupStore('ward-patient-store'); expect(workspaceStore.getState().openWorkspaces.length).toBe(1); expect(wardPatientGroupStore).toBeTruthy(); expect(wardPatientGroupStore?.getState()?.['foo']).toBe(true); + launchWorkspaceGroup('another-sidebar-group', { state: { bar: false, }, workspaceToLaunch: { name: 'transfer-patient-workspace' }, }); + expect(workspaceStore.getState().workspaceGroup?.name).toBe('another-sidebar-group'); + const anotherWorkspaceGroupStore = getWorkspaceGroupStore('another-sidebar-group'); expect(workspaceStore.getState().openWorkspaces.length).toBe(1); expect(anotherWorkspaceGroupStore?.getState()?.['bar']).toBe(false); @@ -625,18 +747,23 @@ describe('workspace system', () => { moduleName: '@openmrs/esm-ward-app', groups: ['ward-patient-store'], }); + const workspaceStore = getWorkspaceStore(); + launchWorkspaceGroup('ward-patient-store', { state: { foo: true, }, workspaceToLaunch: { name: 'ward-patient-workspace' }, }); + const wardPatientGroupStore = getWorkspaceGroupStore('ward-patient-store'); expect(workspaceStore.getState().openWorkspaces.length).toBe(1); expect(wardPatientGroupStore).toBeTruthy(); expect(wardPatientGroupStore?.getState()?.['foo']).toBe(true); + launchWorkspace('transfer-patient-workspace', { bar: false }); + expect(workspaceStore.getState().openWorkspaces.length).toBe(2); expect(wardPatientGroupStore?.getState()?.['foo']).toBe(true); expect(wardPatientGroupStore?.getState()?.['bar']).toBe(false); @@ -655,11 +782,15 @@ describe('workspace system', () => { state: { foo: true }, workspaceToLaunch: { name: 'ward-patient-workspace' }, }); + const workspaceGroupStore = getWorkspaceGroupStore('ward-patient-store'); + expect(workspaceGroupStore).toBeTruthy(); expect(workspaceGroupStore?.getState()?.['foo']).toBe(true); + // test that default options are interpolated when providing options to `closeWorkspace` closeWorkspace('ward-patient-workspace', { ignoreChanges: true }); + expect(workspaceGroupStore?.getState()?.['foo']).toBeUndefined(); expect(workspaceGroupStore?.getState()).toStrictEqual({}); });