Skip to content

Commit

Permalink
Break into select rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
NethmiRodrigo committed Jan 10, 2025
1 parent ed07171 commit cef158e
Show file tree
Hide file tree
Showing 15 changed files with 569 additions and 394 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React, { createContext, useCallback, useContext, useState, type ReactNode } from 'react';
import type { FormField } from '@openmrs/esm-form-engine-lib';
import type { Concept } from '@types';

interface FormFieldContextType {
formField: FormField;
setFormField: React.Dispatch<React.SetStateAction<FormField>>;
concept: Concept;
setConcept: React.Dispatch<React.SetStateAction<Concept>>;
}

const FormFieldContext = createContext<FormFieldContextType | undefined>(undefined);
Expand All @@ -12,8 +15,10 @@ export const FormFieldProvider: React.FC<{
children: ReactNode;
initialFormField: FormField;
isObsGrouped?: boolean;
}> = ({ children, initialFormField, isObsGrouped = false }) => {
selectedConcept?: Concept;
}> = ({ children, initialFormField, isObsGrouped = false, selectedConcept = null }) => {
const [formField, setFormField] = useState<FormField>(initialFormField);
const [concept, setConcept] = useState<Concept | null>(selectedConcept);

const updateObsGroupedQuestion = useCallback(
(updatedObsGroupFormField: FormField) => {
Expand All @@ -33,7 +38,7 @@ export const FormFieldProvider: React.FC<{

return (
<FormFieldContext.Provider
value={{ formField, setFormField: isObsGrouped ? updateObsGroupedQuestion : setFormField }}
value={{ formField, setFormField: isObsGrouped ? updateObsGroupedQuestion : setFormField, concept, setConcept }}
>
{children}
</FormFieldContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@use '@carbon/colors';
@use '@carbon/layout';
@use '@carbon/type';

.label {
margin-bottom: layout.$spacing-02;
}

.error {
width: 100%;
max-width: unset;
padding: '0rem';
margin-bottom: $spacing-05;
}

.loader {
padding: layout.$spacing-04 $spacing-03;
}

.concept {
padding: $spacing-04;
border-bottom: 1px solid colors.$gray-20;

&:last-of-type {
border-bottom: none;
}
}

.conceptList {
background: colors.$white-0;
max-height: 14rem;
overflow-y: auto;
border: 1px solid colors.$gray-20;
border-top: none;
}

.conceptList li:hover {
background-color: colors.$gray-20;
}

.emptyResults {
@include type.type-style('body-compact-01');
color: colors.$gray-70;
min-height: layout.$spacing-05;
}

.oclLauncherBanner {
padding: layout.$spacing-03 layout.$spacing-05;
display: flex;
justify-content: space-between;
align-items: center;
background-color: colors.$gray-50;

p {
color: colors.$white-0;
}
}

.oclLink {
color: colors.$blue-10;
display: flex;
align-items: center;

svg {
margin-left: 0.25rem;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { useConceptId } from '@hooks/useConceptId';
import { useConceptLookup } from '@hooks/useConceptLookup';
import ConceptSearch from './concept-search.component';
import type { Concept } from '@types';

const concepts: Array<Concept> = [
{
uuid: '123',
display: 'Concept 1',
datatype: { uuid: '456', name: 'Coded' },
mappings: [{ display: 'CIEL:1606', conceptMapType: { display: 'SAME-AS' } }],
answers: [
{ uuid: '1', display: 'Answer 1' },
{ uuid: '2', display: 'Answer 2' },
],
},
{
uuid: '456',
display: 'Concept 2',
datatype: { uuid: '456', name: 'Date' },
mappings: [{ display: 'CIEL:1656', conceptMapType: { display: 'SAME-AS' } }],
},
];
const mockUseConceptLookup = jest.mocked(useConceptLookup);
jest.mock('@hooks/useConceptLookup', () => ({
...jest.requireActual('@hooks/useConceptLookup'),
useConceptLookup: jest.fn(),
}));
const mockUseConceptId = jest.mocked(useConceptId);
jest.mock('@hooks/useConceptId', () => ({
...jest.requireActual('@hooks/useConceptId'),
useConceptId: jest.fn(),
}));
const onSelectConcept = jest.fn();

describe('Concept search component', () => {
beforeEach(() => {
mockUseConceptLookup.mockReturnValue({ concepts: concepts, conceptLookupError: null, isLoadingConcepts: false });
mockUseConceptId.mockReturnValue({
concept: null,
conceptName: null,
conceptNameLookupError: null,
isLoadingConcept: false,
});
});
it('renders', () => {
renderComponent();
expect(screen.getByRole('searchbox', { name: /search for a backing concept/i })).toBeInTheDocument();
});

it('allows user to search and select a concept', async () => {
mockUseConceptLookup.mockReturnValue({ concepts: concepts, conceptLookupError: null, isLoadingConcepts: false });
mockUseConceptId.mockReturnValue({
concept: null,
conceptName: null,
conceptNameLookupError: null,
isLoadingConcept: false,
});
const user = userEvent.setup();
renderComponent();

const searchInput = screen.getByRole('searchbox', { name: /search for a backing concept/i });
await user.click(searchInput);
await user.type(searchInput, 'Concept 1');

const conceptMenuItem = await screen.findByRole('menuitem', {
name: /concept 1/i,
});
expect(conceptMenuItem).toBeInTheDocument();
expect(searchInput).toHaveDisplayValue(/concept 1/i);
});

it('shows loading spinner when concept is loading', async () => {
mockUseConceptLookup.mockReturnValue({ concepts: concepts, conceptLookupError: null, isLoadingConcepts: true });
mockUseConceptId.mockReturnValue({
concept: null,
conceptName: null,
conceptNameLookupError: null,
isLoadingConcept: false,
});
const user = userEvent.setup();
renderComponent();

const searchInput = screen.getByRole('searchbox', { name: /search for a backing concept/i });
await user.click(searchInput);
await user.type(searchInput, 'Does not exist');
expect(screen.getByText(/searching\.\.\./i)).toBeInTheDocument();
});

it('displays an error message if the searched concept cannot be found', async () => {
mockUseConceptLookup.mockReturnValue({ concepts: [], conceptLookupError: Error(), isLoadingConcepts: false });
mockUseConceptId.mockReturnValue({
concept: null,
conceptName: null,
conceptNameLookupError: null,
isLoadingConcept: false,
});
renderComponent();

expect(screen.getByText(/error fetching concepts/i)).toBeInTheDocument();
expect(screen.getByText(/please try again\./i)).toBeInTheDocument();
});

it('displays an error with a link to ocl when concept query gives empty results', async () => {
mockUseConceptLookup.mockReturnValue({ concepts: [], conceptLookupError: null, isLoadingConcepts: false });
mockUseConceptId.mockReturnValue({
concept: null,
conceptName: null,
conceptNameLookupError: null,
isLoadingConcept: false,
});
const user = userEvent.setup();
renderComponent();

const searchInput = screen.getByRole('searchbox', { name: /search for a backing concept/i });
await user.click(searchInput);
await user.type(searchInput, 'Does not exist');

expect(screen.getByText(/no concepts were found that match/i)).toBeInTheDocument();
expect(screen.getByText(/can't find a concept\?/i)).toBeInTheDocument();
expect(
screen.getByRole('link', {
name: /search in ocl/i,
}),
).toBeInTheDocument();
});
});

function renderComponent() {
render(<ConceptSearch onSelectConcept={onSelectConcept} />);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@use '@carbon/colors';
@use '@carbon/layout';

.required {
color: colors.$red-60;
margin-left: 0.125rem;
margin-left: $spacing-02;
}
Loading

0 comments on commit cef158e

Please sign in to comment.