Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1785b26
WEB-4654 abstract out Data Recency
henry-tp Jul 14, 2026
e6d9b41
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 14, 2026
cefcc39
WEB-4654 update Data Recency visual styles
henry-tp Jul 14, 2026
fbb5f87
WEB-4654 fix minor spacing issue
henry-tp Jul 14, 2026
693b5e7
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 15, 2026
e86968e
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 15, 2026
453560b
WEB-4654 change directory for FilterByDataRecency
henry-tp Jul 15, 2026
a09b9c2
WEB-4654 pass in custom filter options from adapter component
henry-tp Jul 16, 2026
1a1180a
WEB-4654 clear unneeded props
henry-tp Jul 16, 2026
7739eb0
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 16, 2026
7fee3e6
WEB-4654 add tests for Data Recency filter
henry-tp Jul 16, 2026
675d246
WEB-4654 remove unnecessary tests
henry-tp Jul 17, 2026
332a835
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 17, 2026
41a107a
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 19, 2026
b8087c9
WEB-4654 use MemoryRouter instead of mocking out location hook
henry-tp Jul 19, 2026
1023fb1
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 20, 2026
c384324
WEB-4654 remove unused var
henry-tp Jul 20, 2026
66bc00f
WEB-4654 remove unused var
henry-tp Jul 20, 2026
b48f56c
WEB-4654 update pageName
henry-tp Jul 26, 2026
7e337a1
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 26, 2026
88e3baf
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 26, 2026
990ffa4
WEB-4654 fix tests
henry-tp Jul 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions __tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,34 @@ describe('ClinicPatients', () => {
expect.any(Function),
);
}, TEST_TIMEOUT_MS);

it('maps an applied data recency filter into the getPatientsForClinic query', async () => {
render(
<MockedProviderWrappers>
<ClinicPatients {...defaultProps} />
</MockedProviderWrappers>
);

// Open the Data Recency filter dropdown, pick a device type and window, and apply.
// Match the trigger via its icon label ("Data Recency" alone also matches the
// sortable column header of the same name).
await userEvent.click(screen.getByRole('button', { name: /Filter by last upload/ }));
await userEvent.click(screen.getByRole('radio', { name: /CGM/ }));
await userEvent.click(screen.getByRole('radio', { name: /Within 14 days/ }));
await userEvent.click(screen.getByRole('button', { name: /Apply/ }));

// The from/to date bounds are derived from the current date, so assert their
// presence and 14-day span rather than exact ISO timestamps.
expect(defaultProps.api.clinics.getPatientsForClinic).toHaveBeenLastCalledWith(
'clinicID123',
expect.objectContaining({
'cgm.lastDataFrom': expect.any(String),
'cgm.lastDataTo': expect.any(String),
limit: 50, offset: 0, period: '14d', sortType: 'cgm', sort: '-lastData',
}),
expect.any(Function),
);
}, TEST_TIMEOUT_MS);
});

describe('managing sites', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { thunk } from 'redux-thunk';
import { MemoryRouter } from 'react-router-dom';

import FilterByDataRecency from '@app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency';

const mockStore = configureStore([thunk]);

describe('FilterByDataRecency', () => {
let store;
let wrapper;

const selectedClinicId = 'clinic123';

const setActiveFilters = jest.fn();

const ui = (props = {}) => (
<Provider store={store}>
<MemoryRouter initialEntries={['/clinic-workspace']}>
<FilterByDataRecency
setActiveFilters={setActiveFilters}
{...props}
/>
</MemoryRouter>
</Provider>
);

const renderComponent = (props = {}) => render(ui(props));

beforeEach(() => {
store = mockStore({
blip: {
selectedClinicId,
clinics: { [selectedClinicId]: { id: selectedClinicId } },
},
});

setActiveFilters.mockClear();
});

describe('handleChange', () => {
it('calls setActiveFilters with the applied data recency merged into the existing activeFilters', async () => {
wrapper = renderComponent({ activeFilters: { lastData: null, lastDataType: null, patientTags: ['tag1'] } });

await userEvent.click(screen.getByRole('button', { name: /^Data Recency/ }));
await screen.findByTestId('data-recency-filter-dropdown');

// Correct options exist
expect(screen.getByRole('radio', { name: /Today/ })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: /Within 2 days/ })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: /Within 14 days/ })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: /Within 30 days/ })).toBeInTheDocument();
expect(screen.queryByRole('radio', { name: /Within 7 days/ })).not.toBeInTheDocument();

await userEvent.click(screen.getByRole('radio', { name: /CGM/ }));
await userEvent.click(screen.getByRole('radio', { name: /Within 14 days/ }));
await userEvent.click(screen.getByRole('button', { name: 'Apply' }));

expect(setActiveFilters).toHaveBeenCalledTimes(1);
expect(setActiveFilters).toHaveBeenCalledWith({
lastData: 14,
lastDataType: 'cgm',
patientTags: ['tag1'],
});
});
});

describe('activeFilters passthrough', () => {
it('reflects the active data recency in the pre-selected radios', async () => {
wrapper = renderComponent({ activeFilters: { lastData: 30, lastDataType: 'bgm' } });

await userEvent.click(screen.getByRole('button', { name: /^Data Recency/ }));
await screen.findByTestId('data-recency-filter-dropdown');

expect(screen.getByRole('radio', { name: /BGM/ })).toBeChecked();
expect(screen.getByRole('radio', { name: /CGM/ })).not.toBeChecked();
expect(screen.getByRole('radio', { name: /Within 30 days/ })).toBeChecked();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { thunk } from 'redux-thunk';
import { MemoryRouter } from 'react-router-dom';

import * as actions from '@app/redux/actions';
import DataRecencyFilterDropdown from '@app/pages/clinicworkspace/components/DataRecencyFilterDropdown';
import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';

const mockStore = configureStore([thunk]);

describe('DataRecencyFilterDropdown', () => {
let store;
let wrapper;

const selectedClinicId = 'clinic123';

const filterOptions = [
{ value: 1, label: 'Today' },
{ value: 2, label: 'Within 2 days' },
{ value: 14, label: 'Within 14 days' },
{ value: 30, label: 'Within 30 days' },
];

let onChange = jest.fn();

const ui = (props = {}) => (
<Provider store={store}>
<MemoryRouter initialEntries={['/clinic-workspace']}>
<DataRecencyFilterDropdown
onChange={onChange}
filterOptions={filterOptions}
{...props}
/>
</MemoryRouter>
</Provider>
);

const renderComponent = (props = {}) => render(ui(props));

beforeEach(() => {
store = mockStore({
blip: {
selectedClinicId,
clinics: { [selectedClinicId]: { id: selectedClinicId } },
},
});

onChange.mockClear();
mockTrackMetric.mockClear();
});

describe('filtering for data recency', () => {
it('applies the device type and data recency based on radios selected', async () => {
renderComponent();

// Dropdown closed initially
expect(screen.queryByTestId('data-recency-filter-dropdown')).not.toBeInTheDocument();

// Open the dropdown
await userEvent.click(screen.getByRole('button', { name: /Data Recency/ }));
expect(screen.getByTestId('data-recency-filter-dropdown')).toBeInTheDocument();

// Nothing selected initially
expect(screen.getByRole('radio', { name: /CGM/ })).not.toBeChecked();
expect(screen.getByRole('radio', { name: /BGM/ })).not.toBeChecked();
expect(screen.getByRole('radio', { name: /Within 14 days/ })).not.toBeChecked();

// Select a device type and a data recency window
await userEvent.click(screen.getByRole('radio', { name: /CGM/ }));
await userEvent.click(screen.getByRole('radio', { name: /Within 14 days/ }));

// Applying the filter sets it
await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
expect(onChange).toHaveBeenCalledWith({ lastData: 14, lastDataType: 'cgm' });
expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Last upload apply filter', { clinicId: 'clinic123', dateRange: '14 days', type: 'cgm', pageName: 'Population Health' });

// Dropdown should automatically close
expect(screen.queryByTestId('data-recency-filter-dropdown')).not.toBeInTheDocument();
});

it('disables the Apply button until both a device type and data recency are selected', async () => {
renderComponent();

await userEvent.click(screen.getByRole('button', { name: /Data Recency/ }));

// Disabled with nothing selected
expect(screen.getByRole('button', { name: /Apply/ })).toBeDisabled();

// Still disabled with only a device type selected
await userEvent.click(screen.getByRole('radio', { name: /BGM/ }));
expect(screen.getByRole('button', { name: /Apply/ })).toBeDisabled();

// Enabled once a data recency window is also selected
await userEvent.click(screen.getByRole('radio', { name: /Within 2 days/ }));
expect(screen.getByRole('button', { name: /Apply/ })).toBeEnabled();
});

it('pre-selects the radios matching the active filters', async () => {
renderComponent({ lastData: 30, lastDataType: 'cgm' });

await userEvent.click(screen.getByRole('button', { name: /Data Recency/ }));

expect(screen.getByRole('radio', { name: /CGM/ })).toBeChecked();
expect(screen.getByRole('radio', { name: /BGM/ })).not.toBeChecked();
expect(screen.getByRole('radio', { name: /Within 30 days/ })).toBeChecked();
});

it('clears the filter', async () => {
renderComponent({ lastData: 14, lastDataType: 'cgm' });

await userEvent.click(screen.getByRole('button', { name: /Data Recency/ }));
expect(screen.getByRole('radio', { name: /CGM/ })).toBeChecked();
expect(screen.getByRole('radio', { name: /Within 14 days/ })).toBeChecked();

await userEvent.click(screen.getByRole('button', { name: /Clear/ }));
expect(onChange).toHaveBeenCalledWith({ lastData: null, lastDataType: null });
expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Last upload clear filter', { clinicId: 'clinic123', pageName: 'Population Health' });
expect(screen.queryByTestId('data-recency-filter-dropdown')).not.toBeInTheDocument();
});
});
});
Loading