Skip to main content
Glama
ColumnStatistics.test.tsx27.9 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { MantineProvider } from '@mantine/core'; import { Notifications, notifications } from '@mantine/notifications'; import { ContentType, MedplumClient } from '@medplum/core'; import { MedplumProvider } from '@medplum/react'; import { MemoryRouter } from 'react-router'; import { act, fireEvent, render, screen, waitFor } from '../../test-utils/render'; import { ColumnStatistics } from './ColumnStatistics'; describe('ColumnStatistics', () => { let medplum: MedplumClient; function setup(): void { render( <MedplumProvider medplum={medplum}> <MemoryRouter initialEntries={['/admin/super/db']} initialIndex={0}> <MantineProvider> <Notifications /> <ColumnStatistics /> </MantineProvider> </MemoryRouter> </MedplumProvider> ); } const mockColumnStats = { resourceType: 'Parameters', parameter: [ { name: 'defaultStatisticsTarget', valueInteger: 100, }, { name: 'table', part: [ { name: 'column', part: [ { name: 'name', valueString: 'id' }, { name: 'statisticsTarget', valueInteger: 200 }, { name: 'nullFraction', valueDecimal: 0.0 }, { name: 'avgWidth', valueInteger: 36 }, { name: 'nDistinct', valueDecimal: -1 }, { name: 'correlation', valueDecimal: 0.5 }, { name: 'mostCommonValues', valueString: '{value1,value2,value3}' }, { name: 'mostCommonFreqs', valueString: '{0.1,0.2,0.3}' }, { name: 'histogramBounds', valueString: '{a,b,c}' }, { name: 'mostCommonElems', valueString: '{elem1,elem2}' }, { name: 'mostCommonElemFreqs', valueString: '{0.4,0.5}' }, { name: 'elemCountHistogram', valueString: '{1,2,3}' }, ], }, { name: 'column', part: [ { name: 'name', valueString: 'content' }, { name: 'statisticsTarget', valueInteger: -1 }, { name: 'nullFraction', valueDecimal: 0.1 }, { name: 'avgWidth', valueInteger: 100 }, { name: 'nDistinct', valueDecimal: 50 }, { name: 'correlation', valueDecimal: 0.2 }, ], }, ], }, ], }; beforeEach(() => { const fetch = jest.fn(async (url: string, options?: { method?: string }) => { let status: number | undefined; let body: any; if (url.includes('ValueSet/$expand')) { status = 200; body = { resourceType: 'ValueSet', status: 'active', expansion: { timestamp: '2021-01-01T00:00:00.000Z', contains: [{ code: 'Patient' }], }, }; } else if (url.includes('$db-column-statistics')) { status = 200; body = mockColumnStats; } else if (url.includes('$db-configure-column-statistics') && options?.method === 'POST') { status = 200; body = { resourceType: 'Parameters', parameter: [] }; } else { status = 404; } return { status, headers: { get(name: string): string | undefined { return { 'content-type': ContentType.FHIR_JSON }[name]; }, }, json: jest.fn(async () => body), }; }); medplum = new MedplumClient({ fetch }); jest.useFakeTimers(); jest.spyOn(medplum, 'isSuperAdmin').mockImplementation(() => true); }); afterEach(async () => { await act(async () => notifications.clean()); jest.clearAllMocks(); await act(async () => { jest.runOnlyPendingTimers(); }); jest.useRealTimers(); }); test('Renders and loads column statistics', async () => { setup(); expect(screen.getByPlaceholderText('Table name')).toBeInTheDocument(); expect(screen.getByText('Statistics Target:')).toBeInTheDocument(); await act(async () => { jest.advanceTimersByTime(100); }); expect(screen.getByText('Column')).toBeInTheDocument(); expect(screen.getByText('null_frac')).toBeInTheDocument(); }); test('Selects table and displays column data', async () => { setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); expect(screen.getByText('id')).toBeInTheDocument(); expect(screen.getByText('content')).toBeInTheDocument(); expect(screen.getByText('200')).toBeInTheDocument(); }); test('Select all and deselect all columns', async () => { setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Find the header checkbox (select all) const thead = document.querySelector('thead'); const selectAllCheckbox = thead?.querySelector('input[type="checkbox"]') as HTMLInputElement; expect(selectAllCheckbox).toBeInTheDocument(); // Select all await act(async () => { fireEvent.click(selectAllCheckbox); }); // Deselect all await act(async () => { fireEvent.click(selectAllCheckbox); }); }); test('Select individual row', async () => { setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Find row checkboxes in tbody const tbody = document.querySelector('tbody'); const rowCheckboxes = tbody?.querySelectorAll('input[type="checkbox"]'); const firstRowCheckbox = rowCheckboxes?.[0] as HTMLInputElement; // Select individual row await act(async () => { fireEvent.click(firstRowCheckbox); }); // Deselect individual row await act(async () => { fireEvent.click(firstRowCheckbox); }); }); test('Returns early when submitting without table selected', async () => { const noTableFetch = jest.fn(async (url: string, options?: { method?: string }) => { if (url.includes('ValueSet/$expand')) { return { status: 200, headers: { get: (name: string) => ({ 'content-type': ContentType.FHIR_JSON })[name], }, json: async () => ({ resourceType: 'ValueSet', status: 'active', expansion: { timestamp: '2021-01-01T00:00:00.000Z', contains: [{ code: 'Patient' }] }, }), }; } else if (url.includes('$db-column-statistics')) { return { status: 200, headers: { get: (name: string) => ({ 'content-type': ContentType.FHIR_JSON })[name], }, json: async () => ({ resourceType: 'Parameters', parameter: [] }), }; } else if (url.includes('$db-configure-column-statistics') && options?.method === 'POST') { throw new Error('Should not reach here'); } return { status: 404, headers: { get: () => undefined }, json: async () => ({}) }; }); medplum = new MedplumClient({ fetch: noTableFetch }); jest.spyOn(medplum, 'isSuperAdmin').mockImplementation(() => true); setup(); await act(async () => { jest.advanceTimersByTime(100); }); // Check reset to default to make form submission valid const resetToDefaultCheckbox = screen.getByLabelText(/Reset to default/); await act(async () => { fireEvent.click(resetToDefaultCheckbox); }); await act(async () => { fireEvent.click(screen.getByText('Update')); }); await act(async () => { jest.advanceTimersByTime(100); }); // Form submission returns early (no table selected), no API call made // No notification shown because it returns immediately expect(screen.getByText('Statistics Target:')).toBeInTheDocument(); }); test('Shows notification when submitting without selecting columns', async () => { setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Check reset to default to make the number input not required const resetToDefaultCheckbox = screen.getByLabelText(/Reset to default/); await act(async () => { fireEvent.click(resetToDefaultCheckbox); }); // Submit without selecting columns await act(async () => { fireEvent.click(screen.getByText('Update')); }); await act(async () => { jest.advanceTimersByTime(100); }); await waitFor(() => { expect(screen.getByText('No columns selected')).toBeInTheDocument(); }); }); test('Submits form with selected columns and new statistics target', async () => { setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Select first row const tbody = document.querySelector('tbody'); const firstRowCheckbox = tbody?.querySelector('input[type="checkbox"]') as HTMLInputElement; await act(async () => { fireEvent.click(firstRowCheckbox); }); // Set statistics target - find the NumberInput by looking for an input within a NumberInput wrapper const numberInput = document.querySelector('.mantine-NumberInput-input') as HTMLInputElement; if (numberInput) { await act(async () => { fireEvent.change(numberInput, { target: { value: '500' } }); }); } // Submit await act(async () => { fireEvent.click(screen.getByText('Update')); }); await act(async () => { jest.advanceTimersByTime(100); }); await waitFor(() => { expect(screen.getByText('Done')).toBeInTheDocument(); }); }); test('Submits form with reset to default', async () => { setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Select all rows const thead = document.querySelector('thead'); const selectAllCheckbox = thead?.querySelector('input[type="checkbox"]') as HTMLInputElement; await act(async () => { fireEvent.click(selectAllCheckbox); }); // Check reset to default const resetToDefaultCheckbox = screen.getByLabelText(/Reset to default/); await act(async () => { fireEvent.click(resetToDefaultCheckbox); }); // Submit await act(async () => { fireEvent.click(screen.getByText('Update')); }); await act(async () => { jest.advanceTimersByTime(100); }); await waitFor(() => { expect(screen.getByText('Done')).toBeInTheDocument(); }); }); test('Toggles show more stats', async () => { setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Toggle show more stats const showMoreStatsCheckbox = screen.getByLabelText('Show all column stats'); await act(async () => { fireEvent.click(showMoreStatsCheckbox); }); expect(screen.getByText('most_common_vals')).toBeInTheDocument(); expect(screen.getByText('histogram_bounds')).toBeInTheDocument(); }); test('Toggles show non-default only', async () => { setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Toggle show non-default only const showNonDefaultCheckbox = screen.getByLabelText('Hide columns with default statistics target'); await act(async () => { fireEvent.click(showNonDefaultCheckbox); }); expect(screen.getByText(/hidden columns with default statistics target/)).toBeInTheDocument(); }); test('Opens modal when clicking stat cell', async () => { setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Click on a stat cell const statCell = screen.getByText('36'); await act(async () => { fireEvent.click(statCell); }); await waitFor(() => { expect(screen.getByText('Details')).toBeInTheDocument(); }); }); test('Refresh button triggers reload', async () => { setup(); await act(async () => { jest.advanceTimersByTime(100); }); await act(async () => { fireEvent.click(screen.getByText('Refresh')); }); await act(async () => { jest.advanceTimersByTime(100); }); }); test('Handles API error on column statistics fetch', async () => { const errorFetch = jest.fn(async (url: string) => { if (url.includes('ValueSet/$expand')) { return { status: 200, headers: { get: (name: string) => ({ 'content-type': ContentType.FHIR_JSON })[name], }, json: async () => ({ resourceType: 'ValueSet', status: 'active', expansion: { timestamp: '2021-01-01T00:00:00.000Z', contains: [{ code: 'Patient' }] }, }), }; } else if (url.includes('$db-column-statistics')) { throw new Error('API Error'); } return { status: 404, headers: { get: () => undefined }, json: async () => ({}) }; }); medplum = new MedplumClient({ fetch: errorFetch }); jest.spyOn(medplum, 'isSuperAdmin').mockImplementation(() => true); setup(); await act(async () => { jest.advanceTimersByTime(100); }); await waitFor(() => { expect(screen.getByText('API Error')).toBeInTheDocument(); }); }); test('Handles configure column statistics API error', async () => { const errorFetch = jest.fn(async (url: string, options?: { method?: string }) => { if (url.includes('ValueSet/$expand')) { return { status: 200, headers: { get: (name: string) => ({ 'content-type': ContentType.FHIR_JSON })[name], }, json: async () => ({ resourceType: 'ValueSet', status: 'active', expansion: { timestamp: '2021-01-01T00:00:00.000Z', contains: [{ code: 'Patient' }] }, }), }; } else if (url.includes('$db-column-statistics') && (!options?.method || options?.method === 'GET')) { return { status: 200, headers: { get: (name: string) => ({ 'content-type': ContentType.FHIR_JSON })[name], }, json: async () => mockColumnStats, }; } else if (url.includes('$db-configure-column-statistics') && options?.method === 'POST') { throw new Error('Configure Error'); } return { status: 404, headers: { get: () => undefined }, json: async () => ({}) }; }); medplum = new MedplumClient({ fetch: errorFetch }); jest.spyOn(medplum, 'isSuperAdmin').mockImplementation(() => true); setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Select all rows const thead = document.querySelector('thead'); const selectAllCheckbox = thead?.querySelector('input[type="checkbox"]') as HTMLInputElement; await act(async () => { fireEvent.click(selectAllCheckbox); }); // Check reset to default const resetToDefaultCheckbox = screen.getByLabelText(/Reset to default/); await act(async () => { fireEvent.click(resetToDefaultCheckbox); }); // Submit await act(async () => { fireEvent.click(screen.getByText('Update')); }); await act(async () => { jest.advanceTimersByTime(100); }); await waitFor(() => { expect(screen.getByText('Configure Error')).toBeInTheDocument(); }); }); test('Handles empty column stats response', async () => { const emptyFetch = jest.fn(async (url: string) => { if (url.includes('ValueSet/$expand')) { return { status: 200, headers: { get: (name: string) => ({ 'content-type': ContentType.FHIR_JSON })[name], }, json: async () => ({ resourceType: 'ValueSet', status: 'active', expansion: { timestamp: '2021-01-01T00:00:00.000Z', contains: [{ code: 'Patient' }] }, }), }; } else if (url.includes('$db-column-statistics')) { return { status: 200, headers: { get: (name: string) => ({ 'content-type': ContentType.FHIR_JSON })[name], }, json: async () => ({ resourceType: 'Parameters', parameter: [] }), }; } return { status: 404, headers: { get: () => undefined }, json: async () => ({}) }; }); medplum = new MedplumClient({ fetch: emptyFetch }); jest.spyOn(medplum, 'isSuperAdmin').mockImplementation(() => true); setup(); await act(async () => { jest.advanceTimersByTime(100); }); expect(screen.getByText('Statistics Target:')).toBeInTheDocument(); }); test('Truncates long string values', async () => { const longValueFetch = jest.fn(async (url: string) => { if (url.includes('ValueSet/$expand')) { return { status: 200, headers: { get: (name: string) => ({ 'content-type': ContentType.FHIR_JSON })[name], }, json: async () => ({ resourceType: 'ValueSet', status: 'active', expansion: { timestamp: '2021-01-01T00:00:00.000Z', contains: [{ code: 'Patient' }] }, }), }; } else if (url.includes('$db-column-statistics')) { return { status: 200, headers: { get: (name: string) => ({ 'content-type': ContentType.FHIR_JSON })[name], }, json: async () => ({ resourceType: 'Parameters', parameter: [ { name: 'defaultStatisticsTarget', valueInteger: 100 }, { name: 'table', part: [ { name: 'column', part: [ { name: 'name', valueString: 'longColumn' }, { name: 'statisticsTarget', valueInteger: 100 }, { name: 'nullFraction', valueDecimal: 0.0 }, { name: 'avgWidth', valueInteger: 36 }, { name: 'nDistinct', valueDecimal: -1 }, { name: 'correlation', valueDecimal: 0.5 }, { name: 'mostCommonValues', valueString: 'this_is_a_very_long_string_that_exceeds_thirty_characters_and_should_be_truncated', }, ], }, ], }, ], }), }; } return { status: 404, headers: { get: () => undefined }, json: async () => ({}) }; }); medplum = new MedplumClient({ fetch: longValueFetch }); jest.spyOn(medplum, 'isSuperAdmin').mockImplementation(() => true); setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Toggle show more stats const showMoreStatsCheckbox = screen.getByLabelText('Show all column stats'); await act(async () => { fireEvent.click(showMoreStatsCheckbox); }); // The truncated value should end with '...' expect(screen.getByText(/this_is_a_very_long_string_tha\.\.\./)).toBeInTheDocument(); }); test('Shows statistics target -1 as default', async () => { setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Should show "default (100)" for statistics target -1 (for content column) // Use getAllByText since "default (100)" appears in both the checkbox label and the column const matches = screen.getAllByText(/default \(100\)/); expect(matches.length).toBeGreaterThan(0); }); test('Show non-default with show more stats enabled', async () => { setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Toggle show more stats first const showMoreStatsCheckbox = screen.getByLabelText('Show all column stats'); await act(async () => { fireEvent.click(showMoreStatsCheckbox); }); // Then toggle show non-default only const showNonDefaultCheckbox = screen.getByLabelText('Hide columns with default statistics target'); await act(async () => { fireEvent.click(showNonDefaultCheckbox); }); // Should show message about hidden columns with wider colspan expect(screen.getByText(/hidden columns with default statistics target/)).toBeInTheDocument(); }); test('Table change clears selected rows', async () => { setup(); const input = screen.getByPlaceholderText('Table name') as HTMLInputElement; await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Select a row const tbody = document.querySelector('tbody'); const firstRowCheckbox = tbody?.querySelector('input[type="checkbox"]') as HTMLInputElement; await act(async () => { fireEvent.click(firstRowCheckbox); }); expect(firstRowCheckbox.checked).toBe(true); // Change table await act(async () => { fireEvent.click(input); }); await act(async () => { fireEvent.change(input, { target: { value: 'Patient' } }); }); await act(async () => { jest.advanceTimersByTime(1000); }); await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); }); // Selected rows should be cleared const newFirstRowCheckbox = document.querySelector('tbody input[type="checkbox"]') as HTMLInputElement; expect(newFirstRowCheckbox.checked).toBe(false); }); });

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