From 7a47e5e95a058f61aeb2024ca623a767b173ab47 Mon Sep 17 00:00:00 2001 From: Usama Idriss Kakumba Date: Fri, 1 Nov 2024 14:17:12 +0300 Subject: [PATCH] chore: add unit tests --- __mocks__/order-price-data.mock.ts | 40 ++++++ __mocks__/order-stock-data.mock.ts | 46 +++++++ .../order-price-details.component.tsx | 25 +++- .../components/order-price-details.test.tsx | 91 +++++++++++++ .../order-stock-details.component.tsx | 8 +- .../src/components/order-stock-details.scss | 3 + .../components/order-stock-details.test.tsx | 127 ++++++++++++++++++ .../src/hooks/useOrderPrice.test.ts | 40 ++++++ .../src/hooks/useOrderStockInfo.test.ts | 40 ++++++ 9 files changed, 414 insertions(+), 6 deletions(-) create mode 100644 __mocks__/order-price-data.mock.ts create mode 100644 __mocks__/order-stock-data.mock.ts create mode 100644 packages/esm-patient-orders-app/src/components/order-price-details.test.tsx create mode 100644 packages/esm-patient-orders-app/src/components/order-stock-details.test.tsx create mode 100644 packages/esm-patient-orders-app/src/hooks/useOrderPrice.test.ts create mode 100644 packages/esm-patient-orders-app/src/hooks/useOrderStockInfo.test.ts diff --git a/__mocks__/order-price-data.mock.ts b/__mocks__/order-price-data.mock.ts new file mode 100644 index 0000000000..4948dac1d0 --- /dev/null +++ b/__mocks__/order-price-data.mock.ts @@ -0,0 +1,40 @@ +import type { OrderPriceData } from '@openmrs/esm-patient-orders-app/src/types/order'; + +export const mockOrderPriceData: OrderPriceData = { + resourceType: 'Bundle', + id: 'test-id', + meta: { + lastUpdated: '2024-01-01T00:00:00Z', + }, + type: 'searchset', + link: [ + { + relation: 'self', + url: 'test-url', + }, + ], + entry: [ + { + resource: { + resourceType: 'ChargeItemDefinition', + id: 'test-resource-id', + name: 'Test Item', + status: 'active', + date: '2024-01-01', + propertyGroup: [ + { + priceComponent: [ + { + type: 'base', + amount: { + value: 99.99, + currency: 'USD', + }, + }, + ], + }, + ], + }, + }, + ], +}; diff --git a/__mocks__/order-stock-data.mock.ts b/__mocks__/order-stock-data.mock.ts new file mode 100644 index 0000000000..e6a43f3857 --- /dev/null +++ b/__mocks__/order-stock-data.mock.ts @@ -0,0 +1,46 @@ +export const mockOrderStockData = { + resourceType: 'Bundle', + id: 'test-id', + meta: { + lastUpdated: '2024-01-01T00:00:00Z', + }, + type: 'searchset', + link: [ + { + relation: 'self', + url: 'test-url', + }, + ], + entry: [ + { + resource: { + resourceType: 'InventoryItem', + id: 'test-resource-id', + meta: { + profile: ['test-profile'], + }, + status: 'active', + code: [ + { + coding: [ + { + system: 'test-system', + code: 'test-code', + display: 'Test Item', + }, + ], + }, + ], + name: [ + { + name: 'Test Item', + }, + ], + netContent: { + value: 10, + unit: 'units', + }, + }, + }, + ], +}; diff --git a/packages/esm-patient-orders-app/src/components/order-price-details.component.tsx b/packages/esm-patient-orders-app/src/components/order-price-details.component.tsx index be880c980e..2d33dd7238 100644 --- a/packages/esm-patient-orders-app/src/components/order-price-details.component.tsx +++ b/packages/esm-patient-orders-app/src/components/order-price-details.component.tsx @@ -10,7 +10,7 @@ interface OrderPriceDetailsComponentProps { } const OrderPriceDetailsComponent: React.FC = ({ orderItemUuid }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { data: priceData, isLoading } = useOrderPrice(orderItemUuid); const amount = useMemo(() => { @@ -20,18 +20,35 @@ const OrderPriceDetailsComponent: React.FC = ({ return priceData.entry[0].resource.propertyGroup[0]?.priceComponent[0]?.amount; }, [priceData]); + const formatPrice = ( + amount: { + value: number; + currency: string; + }, + locale: string, + ): string => { + if (!amount) return ''; + + return new Intl.NumberFormat(locale, { + style: 'currency', + currency: amount.currency, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount.value); + }; + if (isLoading || !priceData) { - return ; + return ; } - if (!amount) { + if (!priceData || !amount) { return null; } return (
{t('price', 'Price')}: - {`${amount.currency} ${amount.value}`} + {formatPrice(amount, i18n.language)} ({ + useTranslation: jest.fn(), +})); + +const mockUseTranslation = useTranslation as jest.Mock; + +describe('OrderPriceDetailsComponent', () => { + const mockOrderItemUuid = 'test-uuid'; + + beforeEach(() => { + jest.resetAllMocks(); + mockUseTranslation.mockImplementation(() => ({ + t: (key: string, fallback: string) => fallback, + i18n: { language: 'en-US' }, + })); + }); + + it('renders loading skeleton when data is loading', () => { + (useOrderPrice as jest.Mock).mockReturnValue({ + data: null, + isLoading: true, + }); + + renderWithSwr(); + expect(screen.getByTestId('skeleton-text')).toBeInTheDocument(); + }); + + it('renders nothing when amount is null', () => { + (useOrderPrice as jest.Mock).mockReturnValue({ + data: { + ...mockOrderPriceData, + entry: [], + }, + isLoading: false, + }); + + const { container } = renderWithSwr(); + expect(container).toBeEmptyDOMElement(); + }); + + it('renders price correctly with USD currency', () => { + (useOrderPrice as jest.Mock).mockReturnValue({ + data: mockOrderPriceData, + isLoading: false, + }); + + renderWithSwr(); + + expect(screen.getByText('Price:')).toBeInTheDocument(); + expect(screen.getByText('$99.99')).toBeInTheDocument(); + }); + + it('formats price correctly for different locales', () => { + (useOrderPrice as jest.Mock).mockReturnValue({ + data: mockOrderPriceData, + isLoading: false, + }); + + // Change to German locale for this test + mockUseTranslation.mockImplementation(() => ({ + t: (key: string, fallback: string) => fallback, + i18n: { language: 'de-DE' }, + })); + + renderWithSwr(); + + // German locale uses comma as decimal separator + expect(screen.getByText('99,99 $')).toBeInTheDocument(); + }); + + it('displays tooltip with price disclaimer', () => { + (useOrderPrice as jest.Mock).mockReturnValue({ + data: mockOrderPriceData, + isLoading: false, + }); + + renderWithSwr(); + + expect(screen.getByRole('button')).toBeInTheDocument(); + expect(screen.getByLabelText(/This price is indicative/)).toBeInTheDocument(); + }); +}); diff --git a/packages/esm-patient-orders-app/src/components/order-stock-details.component.tsx b/packages/esm-patient-orders-app/src/components/order-stock-details.component.tsx index 8f2e8a6396..046997b853 100644 --- a/packages/esm-patient-orders-app/src/components/order-stock-details.component.tsx +++ b/packages/esm-patient-orders-app/src/components/order-stock-details.component.tsx @@ -21,8 +21,12 @@ const OrderStockDetailsComponent: React.FC = ({ return resource.status === 'active' && resource.netContent?.value > 0; }, [stockData]); - if (isLoading || !stockData) { - return ; + if (isLoading) { + return ; + } + + if (!stockData) { + return null; } return ( diff --git a/packages/esm-patient-orders-app/src/components/order-stock-details.scss b/packages/esm-patient-orders-app/src/components/order-stock-details.scss index e73e402cff..b31e1035c5 100644 --- a/packages/esm-patient-orders-app/src/components/order-stock-details.scss +++ b/packages/esm-patient-orders-app/src/components/order-stock-details.scss @@ -1,3 +1,4 @@ +@use '@carbon/layout'; @use '@openmrs/esm-styleguide/src/vars' as *; .itemInStock { @@ -9,6 +10,7 @@ .itemInStockIcon { fill: $support-02; + margin-right: layout.$spacing-02; } } @@ -21,5 +23,6 @@ .itemOutOfStockIcon { fill: $danger; + margin-right: layout.$spacing-02; } } diff --git a/packages/esm-patient-orders-app/src/components/order-stock-details.test.tsx b/packages/esm-patient-orders-app/src/components/order-stock-details.test.tsx new file mode 100644 index 0000000000..9527f33c16 --- /dev/null +++ b/packages/esm-patient-orders-app/src/components/order-stock-details.test.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import OrderStockDetailsComponent from './order-stock-details.component'; +import { useOrderStockInfo } from '../hooks/useOrderStockInfo'; +import { renderWithSwr } from 'tools'; +import { useTranslation } from 'react-i18next'; +import { mockOrderStockData } from '../../../../__mocks__/order-stock-data.mock'; + +jest.mock('../hooks/useOrderStockInfo'); +jest.mock('react-i18next', () => ({ + useTranslation: jest.fn(), +})); + +const mockUseTranslation = useTranslation as jest.Mock; + +describe('OrderStockDetailsComponent', () => { + const mockOrderItemUuid = 'test-uuid'; + + beforeEach(() => { + jest.resetAllMocks(); + mockUseTranslation.mockImplementation(() => ({ + t: (key: string, fallback: string) => fallback, + })); + }); + + it('renders loading skeleton when data is loading', () => { + (useOrderStockInfo as jest.Mock).mockReturnValue({ + data: null, + isLoading: true, + }); + + renderWithSwr(); + expect(screen.getByTestId('skeleton-text')).toBeInTheDocument(); + }); + + it('renders nothing when stock data is null', () => { + (useOrderStockInfo as jest.Mock).mockReturnValue({ + data: null, + isLoading: false, + }); + + const { container } = renderWithSwr(); + expect(container).toBeEmptyDOMElement(); + }); + + it('renders "In Stock" when item is active and has positive quantity', () => { + (useOrderStockInfo as jest.Mock).mockReturnValue({ + data: mockOrderStockData, + isLoading: false, + }); + + renderWithSwr(); + + expect(screen.getByText('In Stock')).toBeInTheDocument(); + expect(screen.getByText('CheckmarkFilledIcon')).toBeInTheDocument(); + }); + + it('renders "Out of Stock" when item has zero quantity', () => { + const outOfStockData = { + ...mockOrderStockData, + entry: [ + { + ...mockOrderStockData.entry[0], + resource: { + ...mockOrderStockData.entry[0].resource, + netContent: { + value: 0, + unit: 'units', + }, + }, + }, + ], + }; + + (useOrderStockInfo as jest.Mock).mockReturnValue({ + data: outOfStockData, + isLoading: false, + }); + + renderWithSwr(); + + expect(screen.getByText('Out of Stock')).toBeInTheDocument(); + expect(screen.getByText('CloseFilledIcon')).toBeInTheDocument(); + }); + + it('renders "Out of Stock" when item is inactive', () => { + const inactiveData = { + ...mockOrderStockData, + entry: [ + { + ...mockOrderStockData.entry[0], + resource: { + ...mockOrderStockData.entry[0].resource, + status: 'inactive', + }, + }, + ], + }; + + (useOrderStockInfo as jest.Mock).mockReturnValue({ + data: inactiveData, + isLoading: false, + }); + + renderWithSwr(); + + expect(screen.getByText('Out of Stock')).toBeInTheDocument(); + expect(screen.getByText('CloseFilledIcon')).toBeInTheDocument(); + }); + + it('renders "Out of Stock" when entry array is empty', () => { + const emptyData = { + ...mockOrderStockData, + entry: [], + }; + + (useOrderStockInfo as jest.Mock).mockReturnValue({ + data: emptyData, + isLoading: false, + }); + + renderWithSwr(); + + expect(screen.getByText('Out of Stock')).toBeInTheDocument(); + expect(screen.getByText('CloseFilledIcon')).toBeInTheDocument(); + }); +}); diff --git a/packages/esm-patient-orders-app/src/hooks/useOrderPrice.test.ts b/packages/esm-patient-orders-app/src/hooks/useOrderPrice.test.ts new file mode 100644 index 0000000000..415323e779 --- /dev/null +++ b/packages/esm-patient-orders-app/src/hooks/useOrderPrice.test.ts @@ -0,0 +1,40 @@ +import { act, renderHook } from '@testing-library/react'; +import { type FetchResponse, openmrsFetch } from '@openmrs/esm-framework'; +import { type OrderPriceData } from '../types/order'; +import { useOrderPrice } from './useOrderPrice'; +import { mockOrderPriceData } from '../../../../__mocks__/order-price-data.mock'; + +const mockedOpenmrsFetch = jest.mocked(openmrsFetch); + +describe('useOrderPrice', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns null data when orderItemUuid is not provided', () => { + const { result } = renderHook(() => useOrderPrice('')); + + expect(result.current.data).toBeNull(); + expect(result.current.isLoading).toBeFalsy(); + expect(mockedOpenmrsFetch).not.toHaveBeenCalled(); + }); + + it('fetches and returns price data successfully', async () => { + const mockPromise = Promise.resolve({ + data: mockOrderPriceData, + } as unknown as FetchResponse); + mockedOpenmrsFetch.mockReturnValue(mockPromise); + + const { result } = renderHook(() => useOrderPrice('test-uuid')); + + expect(result.current.data).toBeNull(); + + await act(async () => { + await mockPromise; + }); + + expect(result.current.data).toEqual(mockOrderPriceData); + expect(result.current.isLoading).toBeFalsy(); + expect(mockedOpenmrsFetch).toHaveBeenCalledWith('/ws/fhir2/R4/ChargeItemDefinition?code=test-uuid'); + }); +}); diff --git a/packages/esm-patient-orders-app/src/hooks/useOrderStockInfo.test.ts b/packages/esm-patient-orders-app/src/hooks/useOrderStockInfo.test.ts new file mode 100644 index 0000000000..2a51e55eec --- /dev/null +++ b/packages/esm-patient-orders-app/src/hooks/useOrderStockInfo.test.ts @@ -0,0 +1,40 @@ +import { act, renderHook } from '@testing-library/react'; +import { type FetchResponse, openmrsFetch } from '@openmrs/esm-framework'; +import { type OrderStockData } from '../types/order'; +import { useOrderStockInfo } from './useOrderStockInfo'; +import { mockOrderStockData } from '../../../../__mocks__/order-stock-data.mock'; + +const mockedOpenmrsFetch = jest.mocked(openmrsFetch); + +describe('useOrderStockInfo', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns null data when orderItemUuid is not provided', () => { + const { result } = renderHook(() => useOrderStockInfo('')); + + expect(result.current.data).toBeNull(); + expect(result.current.isLoading).toBeFalsy(); + expect(mockedOpenmrsFetch).not.toHaveBeenCalled(); + }); + + it('fetches and returns stock data successfully', async () => { + const mockPromise = Promise.resolve({ + data: mockOrderStockData, + } as unknown as FetchResponse); + mockedOpenmrsFetch.mockReturnValue(mockPromise); + + const { result } = renderHook(() => useOrderStockInfo('test-uuid')); + + expect(result.current.data).toBeNull(); + + await act(async () => { + await mockPromise; + }); + + expect(result.current.data).toEqual(mockOrderStockData); + expect(result.current.isLoading).toBeFalsy(); + expect(mockedOpenmrsFetch).toHaveBeenCalledWith('/ws/fhir2/R4/InventoryItem?code=test-uuid'); + }); +});