Skip to main content
Glama
SearchPopupMenu.test.tsx18.1 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Button, Menu } from '@mantine/core'; import type { Filter, SearchRequest } from '@medplum/core'; import { Operator, globalSchema } from '@medplum/core'; import type { ResourceType, SearchParameter } from '@medplum/fhirtypes'; import { MockClient } from '@medplum/mock'; import { MedplumProvider } from '@medplum/react-hooks'; import { MemoryRouter } from 'react-router'; import { getFieldDefinitions } from '../SearchControl/SearchControlField'; import { act, fireEvent, render, screen, userEvent } from '../test-utils/render'; import type { SearchPopupMenuProps } from './SearchPopupMenu'; import { SearchPopupMenu } from './SearchPopupMenu'; const medplum = new MockClient(); describe('SearchPopupMenu', () => { async function setup(partialProps: Partial<SearchPopupMenuProps>): Promise<void> { const props = { visible: true, x: 0, y: 0, onPrompt: jest.fn(), onChange: jest.fn(), onClose: jest.fn(), ...partialProps, } as SearchPopupMenuProps; render( <MemoryRouter> <MedplumProvider medplum={medplum}> <Menu closeOnItemClick={false}> <Menu.Target> <Button>Toggle menu</Button> </Menu.Target> <SearchPopupMenu {...props} /> </Menu> </MedplumProvider> </MemoryRouter> ); await toggleMenu(); } test('Invalid resource', async () => { await setup({ search: { resourceType: 'xyz' as ResourceType }, }); }); test('Invalid property', async () => { await setup({ search: { resourceType: 'Patient' }, }); }); test('Date sort', async () => { let currSearch: SearchRequest = { resourceType: 'Patient', }; await setup({ search: currSearch, searchParams: [globalSchema.types['Patient'].searchParams?.['birthdate'] as SearchParameter], onChange: (e) => (currSearch = e), }); const sortOldest = await screen.findByText('Sort Oldest to Newest'); await act(async () => { fireEvent.click(sortOldest); }); expect(currSearch.sortRules).toBeDefined(); expect(currSearch.sortRules?.length).toEqual(1); expect(currSearch.sortRules?.[0].code).toEqual('birthdate'); expect(currSearch.sortRules?.[0].descending).toEqual(false); const sortNewest = await screen.findByText('Sort Newest to Oldest'); await act(async () => { fireEvent.click(sortNewest); }); expect(currSearch.sortRules).toBeDefined(); expect(currSearch.sortRules?.length).toEqual(1); expect(currSearch.sortRules?.[0].code).toEqual('birthdate'); expect(currSearch.sortRules?.[0].descending).toEqual(true); }); test('Date submenu prompt', async () => { const searchParam = globalSchema.types['Patient'].searchParams?.['birthdate'] as SearchParameter; const onPrompt = jest.fn(); await setup({ search: { resourceType: 'Patient', }, searchParams: [searchParam], onPrompt, }); const options = [ { text: 'Equals...', operator: Operator.EQUALS }, { text: 'Does not equal...', operator: Operator.NOT_EQUALS }, { text: 'Before...', operator: Operator.ENDS_BEFORE }, { text: 'After...', operator: Operator.STARTS_AFTER }, { text: 'Between...', operator: Operator.EQUALS }, ]; for (const option of options) { onPrompt.mockClear(); const optionButton = await screen.findByText(option.text); await act(async () => { fireEvent.click(optionButton); }); expect(onPrompt).toHaveBeenCalledWith(searchParam, { code: 'birthdate', operator: option.operator, value: '', } as Filter); } }); test.each([ 'Tomorrow', 'Today', 'Yesterday', 'Next 24 Hours', 'Next Month', 'This Month', 'Last Month', 'Year to date', ])('%s shortcut', async (option) => { let currSearch: SearchRequest = { resourceType: 'Patient', }; await setup({ search: currSearch, searchParams: [globalSchema.types['Patient'].searchParams?.['birthdate'] as SearchParameter], onChange: (e) => (currSearch = e), }); const optionButton = await screen.findByText(option); await act(async () => { fireEvent.click(optionButton); }); expect(currSearch.filters).toBeDefined(); expect(currSearch.filters?.length).toEqual(2); expect(currSearch.filters).toMatchObject([ { code: 'birthdate', operator: Operator.GREATER_THAN_OR_EQUALS, }, { code: 'birthdate', operator: Operator.LESS_THAN_OR_EQUALS, }, ]); }); test('Date missing', async () => { let currSearch: SearchRequest = { resourceType: 'Patient', }; await setup({ search: currSearch, searchParams: [globalSchema.types['Patient'].searchParams?.['birthdate'] as SearchParameter], onChange: (e) => (currSearch = e), }); const options = ['Missing', 'Not missing']; for (const option of options) { const optionButton = await screen.findByText(option); await act(async () => { fireEvent.click(optionButton); }); expect(currSearch.filters).toBeDefined(); expect(currSearch.filters?.length).toEqual(1); expect(currSearch.filters).toMatchObject([ { code: 'birthdate', operator: Operator.MISSING, }, ]); } }); test('Date clear filters', async () => { let currSearch: SearchRequest = { resourceType: 'Patient', filters: [ { code: 'birthdate', operator: Operator.EQUALS, value: '2020-01-01', }, ], }; await setup({ search: currSearch, searchParams: [globalSchema.types['Patient'].searchParams?.['birthdate'] as SearchParameter], onChange: (e) => (currSearch = e), }); const clearButton = await screen.findByText('Clear filters'); await act(async () => { fireEvent.click(clearButton); }); expect(currSearch.filters?.length).toEqual(0); }); test('Quantity sort', async () => { const searchParam = globalSchema.types['Observation'].searchParams?.['value-quantity'] as SearchParameter; let currSearch: SearchRequest = { resourceType: 'Patient', }; await setup({ search: currSearch, searchParams: [searchParam], onChange: (e) => (currSearch = e), }); const sortSmallest = await screen.findByText('Sort Smallest to Largest'); await act(async () => { fireEvent.click(sortSmallest); }); expect(currSearch.sortRules).toBeDefined(); expect(currSearch.sortRules?.length).toEqual(1); expect(currSearch.sortRules?.[0].code).toEqual('value-quantity'); expect(currSearch.sortRules?.[0].descending).toEqual(false); const sortLargest = await screen.findByText('Sort Largest to Smallest'); await act(async () => { fireEvent.click(sortLargest); }); expect(currSearch.sortRules).toBeDefined(); expect(currSearch.sortRules?.length).toEqual(1); expect(currSearch.sortRules?.[0].code).toEqual('value-quantity'); expect(currSearch.sortRules?.[0].descending).toEqual(true); }); test('Quantity submenu prompt', async () => { const searchParam = globalSchema.types['Observation'].searchParams?.['value-quantity'] as SearchParameter; const onPrompt = jest.fn(); await setup({ search: { resourceType: 'Observation', }, searchParams: [searchParam], onPrompt, }); const options = [ { text: 'Equals...', operator: Operator.EQUALS }, { text: 'Does not equal...', operator: Operator.NOT_EQUALS }, { text: 'Greater than...', operator: Operator.GREATER_THAN }, { text: 'Greater than or equal to...', operator: Operator.GREATER_THAN_OR_EQUALS }, { text: 'Less than...', operator: Operator.LESS_THAN }, { text: 'Less than or equal to...', operator: Operator.LESS_THAN_OR_EQUALS }, ]; for (const option of options) { onPrompt.mockClear(); const optionButton = await screen.findByText(option.text); await act(async () => { fireEvent.click(optionButton); }); expect(onPrompt).toHaveBeenCalledWith(searchParam, { code: 'value-quantity', operator: option.operator, value: '', } as Filter); } }); test('Quantity missing', async () => { const searchParam = globalSchema.types['Observation'].searchParams?.['value-quantity'] as SearchParameter; let currSearch: SearchRequest = { resourceType: 'Observation', }; await setup({ search: currSearch, searchParams: [searchParam], onChange: (e) => (currSearch = e), }); const options = ['Missing', 'Not missing']; for (const option of options) { const optionButton = await screen.findByText(option); await act(async () => { fireEvent.click(optionButton); }); expect(currSearch.filters).toBeDefined(); expect(currSearch.filters?.length).toEqual(1); expect(currSearch.filters).toMatchObject([ { code: 'value-quantity', operator: Operator.MISSING, }, ]); } }); test('Quantity clear filters', async () => { const searchParam = globalSchema.types['Observation'].searchParams?.['value-quantity'] as SearchParameter; let currSearch: SearchRequest = { resourceType: 'Observation', filters: [ { code: 'value-quantity', operator: Operator.EQUALS, value: '100', }, ], }; await setup({ search: currSearch, searchParams: [searchParam], onChange: (e) => (currSearch = e), }); const clearButton = await screen.findByText('Clear filters'); await act(async () => { fireEvent.click(clearButton); }); expect(currSearch.filters?.length).toEqual(0); }); test('Reference clear filters', async () => { let currSearch: SearchRequest = { resourceType: 'Patient', filters: [ { code: 'organization', operator: Operator.EQUALS, value: 'Organization/123', }, ], }; await setup({ search: currSearch, searchParams: [globalSchema.types['Patient'].searchParams?.['organization'] as SearchParameter], onChange: (e) => (currSearch = e), }); const clearButton = await screen.findByText('Clear filters'); await act(async () => { fireEvent.click(clearButton); }); expect(currSearch.filters?.length).toEqual(0); }); test('Reference submenu prompt', async () => { const searchParam = globalSchema.types['Patient'].searchParams?.['organization'] as SearchParameter; const onPrompt = jest.fn(); await setup({ search: { resourceType: 'Patient', }, searchParams: [searchParam], onPrompt, }); const options = [ { text: 'Equals...', operator: Operator.EQUALS }, { text: 'Does not equal...', operator: Operator.NOT }, ]; for (const option of options) { onPrompt.mockClear(); const optionButton = await screen.findByText(option.text); await act(async () => { fireEvent.click(optionButton); }); expect(onPrompt).toHaveBeenCalledWith(searchParam, { code: 'organization', operator: option.operator, value: '', } as Filter); } }); test('Reference missing', async () => { const searchParam = globalSchema.types['Patient'].searchParams?.['organization'] as SearchParameter; let currSearch: SearchRequest = { resourceType: 'Patient', }; await setup({ search: currSearch, searchParams: [searchParam], onChange: (e) => (currSearch = e), }); const options = ['Missing', 'Not missing']; for (const option of options) { const optionButton = await screen.findByText(option); await act(async () => { fireEvent.click(optionButton); }); expect(currSearch.filters).toBeDefined(); expect(currSearch.filters?.length).toEqual(1); expect(currSearch.filters).toMatchObject([ { code: 'organization', operator: Operator.MISSING, }, ]); } }); test('Text sort', async () => { let currSearch: SearchRequest = { resourceType: 'Patient', }; await setup({ search: currSearch, searchParams: [globalSchema.types['Patient'].searchParams?.['name'] as SearchParameter], onChange: (e) => (currSearch = e), }); const sortAtoZ = await screen.findByText('Sort A to Z'); await act(async () => { fireEvent.click(sortAtoZ); }); expect(currSearch.sortRules).toBeDefined(); expect(currSearch.sortRules?.length).toEqual(1); expect(currSearch.sortRules?.[0].code).toEqual('name'); expect(currSearch.sortRules?.[0].descending).toEqual(false); const sortZtoA = await screen.findByText('Sort Z to A'); await act(async () => { fireEvent.click(sortZtoA); }); expect(currSearch.sortRules).toBeDefined(); expect(currSearch.sortRules?.length).toEqual(1); expect(currSearch.sortRules?.[0].code).toEqual('name'); expect(currSearch.sortRules?.[0].descending).toEqual(true); }); test('Text clear filters', async () => { let currSearch: SearchRequest = { resourceType: 'Patient', filters: [ { code: 'name', operator: Operator.EQUALS, value: 'Alice', }, ], }; await setup({ search: currSearch, searchParams: [globalSchema.types['Patient'].searchParams?.['name'] as SearchParameter], onChange: (e) => (currSearch = e), }); const clearButton = await screen.findByText('Clear filters'); await act(async () => { fireEvent.click(clearButton); }); expect(currSearch.filters?.length).toEqual(0); }); test('Text submenu prompt', async () => { const searchParam = globalSchema.types['Patient'].searchParams?.['name'] as SearchParameter; const onPrompt = jest.fn(); await setup({ search: { resourceType: 'Patient', }, searchParams: [searchParam], onPrompt, }); const options = [ { text: 'Equals...', operator: Operator.EQUALS }, { text: 'Does not equal...', operator: Operator.NOT }, { text: 'Contains...', operator: Operator.CONTAINS }, { text: 'Does not contain...', operator: Operator.EQUALS }, ]; for (const option of options) { onPrompt.mockClear(); const optionButton = await screen.findByText(option.text); await act(async () => { fireEvent.click(optionButton); }); expect(onPrompt).toHaveBeenCalledWith(searchParam, { code: 'name', operator: option.operator, value: '', } as Filter); } }); test('Text missing', async () => { const searchParam = globalSchema.types['Patient'].searchParams?.['name'] as SearchParameter; let currSearch: SearchRequest = { resourceType: 'Patient', }; await setup({ search: currSearch, searchParams: [searchParam], onChange: (e) => (currSearch = e), }); const options = ['Missing', 'Not missing']; for (const option of options) { const optionButton = await screen.findByText(option); await act(async () => { fireEvent.click(optionButton); }); expect(currSearch.filters).toBeDefined(); expect(currSearch.filters?.length).toEqual(1); expect(currSearch.filters).toMatchObject([ { code: 'name', operator: Operator.MISSING, }, ]); } }); test('Renders meta.versionId', async () => { const search: SearchRequest = { resourceType: 'Patient', fields: ['meta.versionId'], }; const fields = getFieldDefinitions(search); await setup({ search, searchParams: fields[0].searchParams, }); expect(await screen.findByText('Equals...')).toBeDefined(); }); test('Renders _lastUpdated', async () => { const search: SearchRequest = { resourceType: 'Patient', fields: ['_lastUpdated'], }; const fields = getFieldDefinitions(search); await setup({ search: { resourceType: 'Patient', }, searchParams: fields[0].searchParams, }); expect(await screen.findByText('Before...')).toBeDefined(); expect(await screen.findByText('After...')).toBeDefined(); }); test('Search parameter choice', async () => { const search: SearchRequest = { resourceType: 'Observation', fields: ['value[x]'], }; const fields = getFieldDefinitions(search); await setup({ search: { resourceType: 'Observation', }, searchParams: fields[0].searchParams, }); expect(await screen.findByText('Value Quantity')).toBeDefined(); expect(await screen.findByText('Value String')).toBeDefined(); }); test('Only one search parameter on exact match', async () => { globalSchema.types['Observation'].searchParams = { subject: { resourceType: 'SearchParameter', code: 'patient', type: 'reference', expression: 'Observation.patient', } as SearchParameter, }; const search: SearchRequest = { resourceType: 'Observation', fields: ['subject'], }; const fields = getFieldDefinitions(search); await setup({ search: { resourceType: 'Observation', }, searchParams: fields[0].searchParams, }); expect(await screen.findByText('Equals...')).toBeDefined(); expect(screen.queryByText('Patient')).toBeNull(); }); }); async function toggleMenu(): Promise<void> { const toggleMenuButton = await screen.findByText('Toggle menu'); await userEvent.click(toggleMenuButton); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server