diff --git a/__tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js b/__tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js
index 4b01fab9f4..6387975d8f 100644
--- a/__tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js
+++ b/__tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js
@@ -382,146 +382,139 @@ describe('ClinicPatients', () => {
describe('filtering for patients', () => {
afterEach(() => {
- // Clear any persisted filter state between tests
localStorage.clear();
});
- it('should allow filtering by sites', async () => {
+ it('maps an applied tag filter into the getPatientsForClinic query', async () => {
render(
);
- // Open the Sites filter dropdown and filter for 2 sites
- await userEvent.click(screen.getByRole('button', { name: /Sites/ }));
-
- const site1checkbox = screen.getByTestId('clinic-site-filter-option-checkbox-site-1-id');
- const site2checkbox = screen.getByTestId('clinic-site-filter-option-checkbox-site-2-id');
-
- expect(site1checkbox).not.toBeChecked();
- expect(site2checkbox).not.toBeChecked();
+ // Open the Tags filter dropdown, select 2 tags, and apply
+ await userEvent.click(screen.getByRole('button', { name: /Tags/ }));
+ await userEvent.click(screen.getByTestId('tag-filter-option-checkbox-tag1'));
+ await userEvent.click(screen.getByTestId('tag-filter-option-checkbox-tag3'));
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
- await userEvent.click(site1checkbox);
- await userEvent.click(site2checkbox);
+ expect(defaultProps.api.clinics.getPatientsForClinic).toHaveBeenLastCalledWith(
+ 'clinicID123',
+ { tags: ['tag1', 'tag3'], limit: 50, offset: 0, period: '14d', sortType: 'cgm', sort: '-lastData' },
+ expect.any(Function),
+ );
+ }, TEST_TIMEOUT_MS);
- expect(site1checkbox).toBeChecked();
- expect(site2checkbox).toBeChecked();
+ it('maps an applied summary period filter into the getPatientsForClinic query', async () => {
+ render(
+
+
+
+ );
- // Click Apply
+ // Open the Summary Period filter dropdown, select 30 days, and apply
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+ await userEvent.click(screen.getByRole('radio', { name: /30 days/ }));
await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
- expect(defaultProps.api.clinics.getPatientsForClinic).toHaveBeenCalledWith(
+ expect(defaultProps.api.clinics.getPatientsForClinic).toHaveBeenLastCalledWith(
'clinicID123',
- { sites: ['site-1-id', 'site-2-id'], limit: 50, offset: 0, period: '14d', sortType: 'cgm', sort: '-lastData' },
+ { limit: 50, offset: 0, period: '30d', sortType: 'cgm', sort: '-lastData' },
expect.any(Function),
);
-
- expect(defaultProps.trackMetric).toHaveBeenCalledWith(
- 'Clinic - Population Health - Clinic sites filter apply',
- { clinicId: 'clinicID123' },
- );
}, TEST_TIMEOUT_MS);
- it('should allow filtering by for patients with zero sites', async () => {
+ it('maps an applied site filter into the getPatientsForClinic query', async () => {
render(
);
- // Open the Sites filter dropdown and filter for 2 sites
+ // Open the Sites filter dropdown, select 2 sites, and apply
await userEvent.click(screen.getByRole('button', { name: /Sites/ }));
-
- const site1checkbox = screen.getByTestId('clinic-site-filter-option-checkbox-site-1-id');
- const site2checkbox = screen.getByTestId('clinic-site-filter-option-checkbox-site-2-id');
- await userEvent.click(site1checkbox);
- await userEvent.click(site2checkbox);
- expect(site1checkbox).toBeChecked();
- expect(site2checkbox).toBeChecked();
-
- // Click the checkbox to filter for pwds with zero sites. Others should uncheck.
- const zeroSiteCheckbox = screen.getByTestId('clinic-site-filter-option-checkbox-PWDS_WITH_ZERO_SITES');
- await userEvent.click(zeroSiteCheckbox);
- expect(site1checkbox).not.toBeChecked();
- expect(site2checkbox).not.toBeChecked();
-
- // Click Apply. A query of `['_']` should be made for sites.
+ await userEvent.click(screen.getByTestId('clinic-site-filter-option-checkbox-site-1-id'));
+ await userEvent.click(screen.getByTestId('clinic-site-filter-option-checkbox-site-2-id'));
await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
- expect(defaultProps.api.clinics.getPatientsForClinic).toHaveBeenCalledWith(
+ expect(defaultProps.api.clinics.getPatientsForClinic).toHaveBeenLastCalledWith(
'clinicID123',
- { sites: ['_'], limit: 50, offset: 0, period: '14d', sortType: 'cgm', sort: '-lastData' },
+ { sites: ['site-1-id', 'site-2-id'], limit: 50, offset: 0, period: '14d', sortType: 'cgm', sort: '-lastData' },
expect.any(Function),
);
}, TEST_TIMEOUT_MS);
- it('should allow filtering by tags', async () => {
+ it('maps an applied data recency filter into the getPatientsForClinic query', async () => {
render(
);
- // Open the Tags filter dropdown and filter for 2 sites
- await userEvent.click(screen.getByRole('button', { name: /Tags/ }));
-
- const tag1checkbox = screen.getByTestId('tag-filter-option-checkbox-tag1');
- const tag3checkbox = screen.getByTestId('tag-filter-option-checkbox-tag3');
-
- expect(tag1checkbox).not.toBeChecked();
- expect(tag3checkbox).not.toBeChecked();
-
- await userEvent.click(tag1checkbox);
- await userEvent.click(tag3checkbox);
-
- expect(tag1checkbox).toBeChecked();
- expect(tag3checkbox).toBeChecked();
-
- // Click Apply
+ // 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',
- { tags: ['tag1', 'tag3'], limit: 50, offset: 0, period: '14d', sortType: 'cgm', sort: '-lastData' },
+ 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),
);
-
- expect(defaultProps.trackMetric).toHaveBeenCalledWith(
- 'Clinic - Population Health - Patient tag filter apply',
- { clinicId: 'clinicID123' },
- );
}, TEST_TIMEOUT_MS);
- it('should allow filtering by for patients with zero tags', async () => {
+ it('maps an applied time in range filter into the getPatientsForClinic query', async () => {
render(
);
- // Open the Tags filter dropdown and filter for 2 tags
- await userEvent.click(screen.getByRole('button', { name: /Tags/ }));
+ // Open the % Time in Range filter dropdown, select a range, and apply.
+ await userEvent.click(screen.getByRole('button', { name: /Filter by Time in Range/ }));
+ await userEvent.click(screen.getByRole('checkbox', { name: /Not meeting TIR/ }));
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
- const tag1checkbox = screen.getByTestId('tag-filter-option-checkbox-tag1');
- const tag2checkbox = screen.getByTestId('tag-filter-option-checkbox-tag2');
- await userEvent.click(tag1checkbox);
- await userEvent.click(tag2checkbox);
- expect(tag1checkbox).toBeChecked();
- expect(tag2checkbox).toBeChecked();
+ // Selecting ranges scopes the query to standard target ranges and maps each
+ // selected range into a `cgm.` comparator threshold (fraction of time).
+ expect(defaultProps.api.clinics.getPatientsForClinic).toHaveBeenLastCalledWith(
+ 'clinicID123',
+ expect.objectContaining({
+ omitNonStandardRanges: true,
+ 'cgm.timeInTargetPercent': '<=0.7',
+ limit: 50, offset: 0, period: '14d', sortType: 'cgm', sort: '-lastData',
+ }),
+ expect.any(Function),
+ );
+ }, TEST_TIMEOUT_MS);
- // Click the checkbox to filter for pwds with zero tags. Others should uncheck.
- const zeroTagCheckbox = screen.getByTestId('tag-filter-option-checkbox-PWDS_WITH_ZERO_TAGS');
- await userEvent.click(zeroTagCheckbox);
- expect(tag1checkbox).not.toBeChecked();
- expect(tag2checkbox).not.toBeChecked();
+ it('maps an applied cgm use filter into the getPatientsForClinic query', async () => {
+ render(
+
+
+
+ );
- // Click Apply. A query of `['_']` should be made for sites.
+ // Open the % CGM Use filter dropdown, select a range, and apply.
+ await userEvent.click(screen.getByRole('button', { name: /CGM Use/ }));
+ await userEvent.click(screen.getByRole('radio', { name: /Less than 70%/ }));
await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
- expect(defaultProps.api.clinics.getPatientsForClinic).toHaveBeenCalledWith(
+ expect(defaultProps.api.clinics.getPatientsForClinic).toHaveBeenLastCalledWith(
'clinicID123',
- { tags: ['_'], limit: 50, offset: 0, period: '14d', sortType: 'cgm', sort: '-lastData' },
+ expect.objectContaining({
+ 'cgm.timeCGMUsePercent': '<0.7',
+ limit: 50, offset: 0, period: '14d', sortType: 'cgm', sort: '-lastData',
+ }),
expect.any(Function),
);
}, TEST_TIMEOUT_MS);
diff --git a/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/AppliedFiltersList.test.js b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/AppliedFiltersList.test.js
new file mode 100644
index 0000000000..99562ba4e1
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/AppliedFiltersList.test.js
@@ -0,0 +1,162 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import configureStore from 'redux-mock-store';
+import { thunk } from 'redux-thunk';
+import { Provider } from 'react-redux';
+import { ThemeProvider } from 'theme-ui';
+
+import theme from '@app/themes/baseTheme';
+import AppliedFiltersList from '@app/pages/clinicworkspace/clinicPatientsFilters/AppliedFiltersList';
+import { defaultFilterState, SPECIAL_FILTER_STATES } from '@app/pages/clinicworkspace/useClinicPatientsFilters';
+
+const mockStore = configureStore([thunk]);
+
+const FULLY_ACTIVE_FILTERS = {
+ ...defaultFilterState,
+ lastData: 14,
+ lastDataType: 'cgm',
+ timeInRange: ['timeInTargetPercent', 'timeInVeryLowPercent'],
+ patientTags: ['tag1', 'tag2'],
+ clinicSites: ['site1', 'site2'],
+};
+
+const buildState = ({
+ fetchedPatientCount = 5,
+ patientListSearchTextInput = '',
+} = {}) => ({
+ blip: {
+ selectedClinicId: 'clinic123',
+ clinics: {
+ 'clinic123': {
+ id: 'clinic123',
+ fetchedPatientCount,
+ patientTags: [
+ { id: 'tag1', name: 'Tag One' },
+ { id: 'tag2', name: 'Tag Two' },
+ ],
+ sites: [
+ { id: 'site1', name: 'Site Alpha' },
+ { id: 'site2', name: 'Site Bravo' },
+ ],
+ },
+ },
+ patientListFilters: { patientListSearchTextInput },
+ },
+});
+
+const renderList = ({
+ activeFilters = defaultFilterState,
+ setActiveFilters = jest.fn(),
+ onClearSearch = jest.fn(),
+ onResetFilters = jest.fn(),
+ state = buildState(),
+} = {}) => {
+ const store = mockStore(state);
+
+ const utils = render(
+
+
+
+
+
+ );
+
+ return { ...utils, setActiveFilters, onClearSearch, onResetFilters };
+};
+
+describe('AppliedFiltersList', () => {
+ describe('clear/reset controls', () => {
+ it('shows a "Reset Filters" control that fires onResetFilters when only filters are active', async () => {
+ const { onResetFilters, onClearSearch } = renderList({
+ activeFilters: { ...defaultFilterState, timeInRange: ['timeInTargetPercent'] },
+ });
+
+ await userEvent.click(screen.getByRole('button', { name: 'Reset All Filters' }));
+
+ expect(onResetFilters).toHaveBeenCalledTimes(1);
+ expect(onClearSearch).not.toHaveBeenCalled();
+ });
+
+ it('shows a "Clear Search" control that fires onClearSearch when only a search is active', async () => {
+ const { onClearSearch, onResetFilters } = renderList({
+ activeFilters: defaultFilterState,
+ state: buildState({ patientListSearchTextInput: 'john' }),
+ });
+
+ await userEvent.click(screen.getByRole('button', { name: 'Clear Search' }));
+
+ expect(onClearSearch).toHaveBeenCalledTimes(1);
+ expect(onResetFilters).not.toHaveBeenCalled();
+ });
+
+ it('shows both controls, each wired to its own callback, when a filter and a search are both active', async () => {
+ const { onClearSearch, onResetFilters } = renderList({
+ activeFilters: { ...defaultFilterState, timeInRange: ['timeInTargetPercent'] },
+ state: buildState({ patientListSearchTextInput: 'john' }),
+ });
+
+ await userEvent.click(screen.getByRole('button', { name: 'Reset All Filters' }));
+ await userEvent.click(screen.getByRole('button', { name: 'Clear Search' }));
+
+ expect(onResetFilters).toHaveBeenCalledTimes(1);
+ expect(onClearSearch).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('removing filters fires setActiveFilters correctly', () => {
+ it('resets lastData and lastDataType to their defaults when the data-recency chip is removed', async () => {
+ const { setActiveFilters } = renderList({ activeFilters: FULLY_ACTIVE_FILTERS });
+
+ await userEvent.click(screen.getByLabelText('Remove CGM data within 14 days filter'));
+
+ expect(setActiveFilters).toHaveBeenCalledTimes(1);
+ expect(setActiveFilters).toHaveBeenCalledWith({
+ ...FULLY_ACTIVE_FILTERS,
+ lastData: defaultFilterState.lastData,
+ lastDataType: defaultFilterState.lastDataType,
+ });
+ });
+
+ it('removes only the clicked time-in-range value, preserving the others', async () => {
+ const { setActiveFilters } = renderList({ activeFilters: FULLY_ACTIVE_FILTERS });
+
+ await userEvent.click(screen.getByLabelText('Remove %TIR = Not in Range filter'));
+
+ expect(setActiveFilters).toHaveBeenCalledTimes(1);
+ expect(setActiveFilters).toHaveBeenCalledWith({
+ ...FULLY_ACTIVE_FILTERS,
+ timeInRange: ['timeInVeryLowPercent'],
+ });
+ });
+
+ it('removes only the clicked patient tag, preserving the others', async () => {
+ const { setActiveFilters } = renderList({ activeFilters: FULLY_ACTIVE_FILTERS });
+
+ await userEvent.click(screen.getByLabelText('Remove Tag One filter'));
+
+ expect(setActiveFilters).toHaveBeenCalledTimes(1);
+ expect(setActiveFilters).toHaveBeenCalledWith({
+ ...FULLY_ACTIVE_FILTERS,
+ patientTags: ['tag2'],
+ });
+ });
+
+ it('removes only the clicked clinic site, preserving the others', async () => {
+ const { setActiveFilters } = renderList({ activeFilters: FULLY_ACTIVE_FILTERS });
+
+ await userEvent.click(screen.getByLabelText('Remove Site Alpha filter'));
+
+ expect(setActiveFilters).toHaveBeenCalledTimes(1);
+ expect(setActiveFilters).toHaveBeenCalledWith({
+ ...FULLY_ACTIVE_FILTERS,
+ clinicSites: ['site2'],
+ });
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByCGMUse.test.js b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByCGMUse.test.js
new file mode 100644
index 0000000000..f0a5326f9d
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByCGMUse.test.js
@@ -0,0 +1,73 @@
+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 FilterByCGMUse from '@app/pages/clinicworkspace/clinicPatientsFilters/FilterByCGMUse';
+
+const mockStore = configureStore([thunk]);
+
+describe('FilterByCGMUse', () => {
+ let store;
+
+ const selectedClinicId = 'clinic123';
+
+ const setActiveFilters = jest.fn();
+
+ const ui = (props = {}) => (
+
+
+
+
+
+ );
+
+ 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 cgm use merged into the existing activeFilters', async () => {
+ renderComponent({ activeFilters: { timeCGMUsePercent: null, patientTags: ['tag1'] } });
+
+ await userEvent.click(screen.getByRole('button', { name: /CGM Use/ }));
+ await screen.findByTestId('cgm-use-filter-dropdown');
+
+ await userEvent.click(screen.getByRole('radio', { name: /Less than 70%/ }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ expect(setActiveFilters).toHaveBeenCalledTimes(1);
+ expect(setActiveFilters).toHaveBeenCalledWith({
+ timeCGMUsePercent: '<0.7',
+ patientTags: ['tag1'],
+ });
+ });
+ });
+
+ describe('activeFilters passthrough', () => {
+ it('reflects the active cgm use in the pre-selected radio', async () => {
+ renderComponent({ activeFilters: { timeCGMUsePercent: '>=0.7' } });
+
+ await userEvent.click(screen.getByRole('button', { name: /CGM Use/ }));
+ await screen.findByTestId('cgm-use-filter-dropdown');
+
+ expect(screen.getByRole('radio', { name: /70% or more/ })).toBeChecked();
+ expect(screen.getByRole('radio', { name: /Less than 70%/ })).not.toBeChecked();
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js
new file mode 100644
index 0000000000..2d3c9210e7
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js
@@ -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 = {}) => (
+
+
+
+
+
+ );
+
+ 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();
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySites.test.js b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySites.test.js
new file mode 100644
index 0000000000..f7fdd3575c
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySites.test.js
@@ -0,0 +1,120 @@
+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 FilterBySites from '@app/pages/clinicworkspace/clinicPatientsFilters/FilterBySites';
+import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
+import useIsClinicAdmin from '@app/pages/clinicworkspace/useIsClinicAdmin';
+
+jest.mock('@app/pages/clinicworkspace/useIsClinicAdmin');
+
+jest.mock('@app/redux/actions', () => ({
+ async: { fetchClinicSites: jest.fn().mockReturnValue({ type: 'FETCH_CLINIC_SITES' }) },
+}));
+
+const mockStore = configureStore([thunk]);
+
+describe('FilterBySites', () => {
+ let store;
+ let wrapper;
+
+ const api = { some: 'api' };
+ const selectedClinicId = 'clinic123';
+
+ const clinicSiteDefs = [
+ { id: 'site1', name: 'Site One' },
+ { id: 'site2', name: 'Site Two' },
+ ];
+
+ const setActiveFilters = jest.fn();
+ const setShowClinicSitesDialog = jest.fn();
+
+ useIsClinicAdmin.mockReturnValue(true);
+
+ const ui = (props = {}) => (
+
+
+
+
+
+ );
+
+ const renderComponent = (props = {}) => render(ui(props));
+
+ beforeEach(() => {
+ store = mockStore({
+ blip: {
+ selectedClinicId,
+ clinics: { [selectedClinicId]: { id: selectedClinicId, sites: clinicSiteDefs } },
+ },
+ });
+
+ setActiveFilters.mockClear();
+ setShowClinicSitesDialog.mockClear();
+ });
+
+ describe('handleChange', () => {
+ it('calls setActiveFilters with the applied sites merged into the existing activeFilters', async () => {
+ wrapper = renderComponent({ activeFilters: { clinicSites: [], patientTags: ['tag1'] } });
+
+ await userEvent.click(screen.getByRole('button', { name: /^Clinic Sites/ }));
+ await screen.findByTestId('clinic-site-filter-option-checkbox-site1');
+
+ await userEvent.click(screen.getByTestId('clinic-site-filter-option-checkbox-site1'));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ expect(setActiveFilters).toHaveBeenCalledTimes(1);
+ expect(setActiveFilters).toHaveBeenCalledWith({
+ clinicSites: ['site1'],
+ patientTags: ['tag1'],
+ });
+ });
+ });
+
+ describe('onClickEditSites visibility', () => {
+ it('offers the edit-sites control to clinic admins only', async () => {
+ // Hidden if not admin
+ useIsClinicAdmin.mockReturnValue(false);
+ const { rerender } = renderComponent();
+
+ expect(screen.queryByRole('button', { name: 'Edit Sites' })).not.toBeInTheDocument();
+
+ // Visible if not admin
+ useIsClinicAdmin.mockReturnValue(true);
+ rerender(ui());
+
+ await userEvent.click(screen.getByRole('button', { name: /^Clinic Sites/ }));
+ await screen.findByTestId('clinic-site-filter-option-checkbox-site1');
+
+ const editButton = screen.getByRole('button', { name: 'Edit Sites' });
+ expect(editButton).toBeInTheDocument();
+
+ await userEvent.click(editButton);
+
+ expect(actions.async.fetchClinicSites).toHaveBeenCalledWith(api, selectedClinicId);
+ expect(setShowClinicSitesDialog).toHaveBeenCalledWith(true);
+ expect(mockTrackMetric).toHaveBeenCalledWith(
+ 'Clinic - Edit clinic sites open',
+ expect.objectContaining({ clinicId: selectedClinicId, source: 'Filter menu', pageName: 'Population Health' })
+ );
+ });
+ });
+
+ describe('clinicSites passthrough', () => {
+ it('reflects the active clinic sites in the filter count', () => {
+ wrapper = renderComponent({ activeFilters: { clinicSites: ['site1', 'site2'] } });
+
+ expect(screen.getByLabelText('clinic site count')).toHaveTextContent('2');
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySummaryPeriod.test.js b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySummaryPeriod.test.js
new file mode 100644
index 0000000000..d5955de405
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySummaryPeriod.test.js
@@ -0,0 +1,78 @@
+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 FilterBySummaryPeriod from '@app/pages/clinicworkspace/clinicPatientsFilters/FilterBySummaryPeriod';
+
+const mockStore = configureStore([thunk]);
+
+describe('FilterBySummaryPeriod', () => {
+ let store;
+ let wrapper;
+
+ const selectedClinicId = 'clinic123';
+
+ const setActiveSummaryPeriod = jest.fn();
+
+ const ui = (props = {}) => (
+
+
+
+
+
+ );
+
+ const renderComponent = (props = {}) => render(ui(props));
+
+ beforeEach(() => {
+ store = mockStore({
+ blip: {
+ selectedClinicId,
+ clinics: { [selectedClinicId]: { id: selectedClinicId } },
+ },
+ });
+
+ setActiveSummaryPeriod.mockClear();
+ });
+
+ describe('handleChange', () => {
+ it('calls setActiveSummaryPeriod with the newly selected period', async () => {
+ wrapper = renderComponent({ activeSummaryPeriod: '14d' });
+
+ // Open the dropdown via the trigger's icon label (its text label is the dynamic
+ // "Summarizing ..." string that changes with the active period).
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+ await screen.findByRole('radio', { name: /30 days/ });
+
+ await userEvent.click(screen.getByRole('radio', { name: /30 days/ }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // The period value is passed straight through, not merged into an activeFilters object
+ expect(setActiveSummaryPeriod).toHaveBeenCalledTimes(1);
+ expect(setActiveSummaryPeriod).toHaveBeenCalledWith('30d');
+ });
+ });
+
+ describe('activeSummaryPeriod passthrough', () => {
+ it('reflects the active summary period in the trigger label and the pre-selected radio', async () => {
+ wrapper = renderComponent({ activeSummaryPeriod: '7d' });
+
+ // Trigger label reflects the active period
+ expect(screen.getByRole('button', { name: /Summarizing 7 days of data/ })).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+
+ expect(screen.getByRole('radio', { name: /24 hours/ })).not.toBeChecked();
+ expect(screen.getByRole('radio', { name: /7 days/ })).toBeChecked();
+ expect(screen.getByRole('radio', { name: /14 days/ })).not.toBeChecked();
+ expect(screen.getByRole('radio', { name: /30 days/ })).not.toBeChecked();
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTags.test.js b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTags.test.js
new file mode 100644
index 0000000000..3a8d5bb5f9
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTags.test.js
@@ -0,0 +1,122 @@
+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 FilterByTags from '@app/pages/clinicworkspace/clinicPatientsFilters/FilterByTags';
+import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
+import useIsClinicAdmin from '@app/pages/clinicworkspace/useIsClinicAdmin';
+
+jest.mock('@app/pages/clinicworkspace/useIsClinicAdmin');
+
+jest.mock('@app/redux/actions', () => ({
+ async: {
+ fetchClinicPatientTags: jest.fn().mockReturnValue({ type: 'FETCH_CLINIC_PATIENT_TAGS' }),
+ },
+}));
+
+const mockStore = configureStore([thunk]);
+
+describe('FilterByTags', () => {
+ let store;
+ let wrapper;
+
+ const api = { some: 'api' };
+ const selectedClinicId = 'clinic123';
+
+ const patientTagDefs = [
+ { id: 'tag1', name: 'Tag One' },
+ { id: 'tag2', name: 'Tag Two' },
+ ];
+
+ const setActiveFilters = jest.fn();
+ const setShowClinicPatientTagsDialog = jest.fn();
+
+ useIsClinicAdmin.mockReturnValue(true);
+
+ const ui = (props = {}) => (
+
+
+
+
+
+ );
+
+ const renderComponent = (props = {}) => render(ui(props));
+
+ beforeEach(() => {
+ store = mockStore({
+ blip: {
+ selectedClinicId,
+ clinics: { [selectedClinicId]: { id: selectedClinicId, patientTags: patientTagDefs } },
+ },
+ });
+
+ setActiveFilters.mockClear();
+ setShowClinicPatientTagsDialog.mockClear();
+ });
+
+ describe('handleChange', () => {
+ it('calls setActiveFilters with the applied tags merged into the existing activeFilters', async () => {
+ wrapper = renderComponent({ activeFilters: { patientTags: [], clinicSites: ['siteX'] } });
+
+ await userEvent.click(screen.getByRole('button', { name: /^Tags/ }));
+ await screen.findByTestId('tag-filter-option-checkbox-tag1');
+
+ await userEvent.click(screen.getByTestId('tag-filter-option-checkbox-tag1'));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ expect(setActiveFilters).toHaveBeenCalledTimes(1);
+ expect(setActiveFilters).toHaveBeenCalledWith({
+ patientTags: ['tag1'],
+ clinicSites: ['siteX'],
+ });
+ });
+ });
+
+ describe('onClickEditTags visibility', () => {
+ it('offers the edit-tags control to clinic admins only', async () => {
+ // Hidden if not admin
+ useIsClinicAdmin.mockReturnValue(false);
+ const { rerender } = renderComponent();
+
+ expect(screen.queryByRole('button', { name: 'Edit Tags' })).not.toBeInTheDocument();
+
+ // Visible if not admin
+ useIsClinicAdmin.mockReturnValue(true);
+ rerender(ui());
+
+ await userEvent.click(screen.getByRole('button', { name: /^Tags/ }));
+ await screen.findByTestId('tag-filter-option-checkbox-tag1');
+
+ const editButton = screen.getByRole('button', { name: 'Edit Tags' });
+ expect(editButton).toBeInTheDocument();
+
+ await userEvent.click(editButton);
+
+ expect(actions.async.fetchClinicPatientTags).toHaveBeenCalledWith(api, selectedClinicId);
+ expect(setShowClinicPatientTagsDialog).toHaveBeenCalledWith(true);
+ expect(mockTrackMetric).toHaveBeenCalledWith(
+ 'Clinic - Edit clinic tags open',
+ expect.objectContaining({ clinicId: selectedClinicId, source: 'Filter menu', pageName: 'Population Health' })
+ );
+ });
+ });
+
+ describe('patientTags passthrough', () => {
+ it('reflects the active patient tags in the filter count', () => {
+ wrapper = renderComponent({ activeFilters: { patientTags: ['tag1', 'tag2'] } });
+
+ expect(screen.getByLabelText('filter count')).toHaveTextContent('2');
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTimeInRange.test.js b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTimeInRange.test.js
new file mode 100644
index 0000000000..687c08e28e
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTimeInRange.test.js
@@ -0,0 +1,79 @@
+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 { useFlags } from 'launchdarkly-react-client-sdk';
+import FilterByTimeInRange from '@app/pages/clinicworkspace/clinicPatientsFilters/FilterByTimeInRange';
+
+jest.mock('launchdarkly-react-client-sdk');
+
+const mockStore = configureStore([thunk]);
+
+describe('FilterByTimeInRange', () => {
+ let store;
+
+ const selectedClinicId = 'clinic123';
+
+ const setActiveFilters = jest.fn();
+
+ useFlags.mockReturnValue({ showExtremeHigh: false });
+
+ const ui = (props = {}) => (
+
+
+
+
+
+ );
+
+ 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 time in range filters merged into the existing activeFilters', async () => {
+ renderComponent({ activeFilters: { timeInRange: [], patientTags: ['tag1'] } });
+
+ await userEvent.click(screen.getByRole('button', { name: /Time in Range/ }));
+ await screen.findByRole('checkbox', { name: /Very High/ });
+
+ await userEvent.click(screen.getByRole('checkbox', { name: /Very High/ }));
+ await userEvent.click(screen.getByRole('checkbox', { name: /Very Low/ }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ expect(setActiveFilters).toHaveBeenCalledTimes(1);
+ expect(setActiveFilters).toHaveBeenCalledWith({
+ timeInRange: ['timeInVeryHighPercent', 'timeInVeryLowPercent'],
+ patientTags: ['tag1'],
+ });
+ });
+ });
+
+ describe('timeInRange passthrough', () => {
+ it('pre-selects the checkboxes matching the active filters', async () => {
+ renderComponent({ activeFilters: { timeInRange: ['timeInVeryLowPercent'] } });
+
+ await userEvent.click(screen.getByRole('button', { name: /Time in Range/ }));
+ await screen.findByRole('checkbox', { name: /Very Low/ });
+
+ expect(screen.getByRole('checkbox', { name: /Very Low/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Very High/ })).not.toBeChecked();
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/ActiveFiltersTray.test.js b/__tests__/unit/app/pages/clinicworkspace/components/ActiveFiltersTray.test.js
new file mode 100644
index 0000000000..f0e378517f
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/components/ActiveFiltersTray.test.js
@@ -0,0 +1,125 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import configureStore from 'redux-mock-store';
+import { thunk } from 'redux-thunk';
+import { Provider } from 'react-redux';
+import { ThemeProvider } from 'theme-ui';
+import '@app/core/language';
+
+import theme from '@app/themes/baseTheme';
+import ActiveFiltersTray from '@app/pages/clinicworkspace/components/ActiveFiltersTray';
+import { defaultFilterState, SPECIAL_FILTER_STATES } from '@app/pages/clinicworkspace/useClinicPatientsFilters';
+
+const mockStore = configureStore([thunk]);
+
+const buildState = ({ fetchedPatientCount = 5 } = {}) => ({
+ blip: {
+ selectedClinicId: 'clinic123',
+ clinics: {
+ 'clinic123': {
+ id: 'clinic123',
+ fetchedPatientCount,
+ patientTags: [
+ { id: 'tag1', name: 'Tag One' },
+ { id: 'tag2', name: 'Tag Two' },
+ ],
+ sites: [
+ { id: 'site1', name: 'Site Alpha' },
+ { id: 'site2', name: 'Site Bravo' },
+ ],
+ },
+ },
+ },
+});
+
+const renderTray = ({
+ filters = defaultFilterState,
+ hasSearchActive = false,
+ onRemoveFilter = jest.fn(),
+ state = buildState(),
+} = {}) => {
+ const store = mockStore(state);
+
+ const utils = render(
+
+
+
+
+
+ );
+
+ return { ...utils, onRemoveFilter };
+};
+
+describe('ActiveFiltersTray', () => {
+ describe('patient count header', () => {
+ it('renders the fetched patient count', () => {
+ renderTray({ state: buildState({ fetchedPatientCount: 5 }) });
+
+ expect(screen.getByText('Showing 5 patients')).toBeInTheDocument();
+ });
+
+ it('notes the count reflects the search when a search is active', () => {
+ renderTray({ hasSearchActive: true, state: buildState({ fetchedPatientCount: 5 }) });
+
+ expect(screen.getByText('Showing 5 patients that match your search')).toBeInTheDocument();
+ });
+ });
+
+ describe('primary filter chips', () => {
+ it('renders a time-in-range filter under the "with" prefix using its expected label', () => {
+ renderTray({ filters: { ...defaultFilterState, timeInRange: ['timeInTargetPercent'] } });
+
+ expect(screen.getByText('with')).toBeInTheDocument();
+ expect(screen.getByText('%TIR = Not in Range')).toBeInTheDocument();
+ });
+
+ it('renders a data-recency filter with its expected label', () => {
+ renderTray({ filters: { ...defaultFilterState, lastData: 14, lastDataType: 'cgm' } });
+
+ expect(screen.getByText('CGM data within 14 days')).toBeInTheDocument();
+ });
+
+ it('renders a CGM-use filter with its expected label', () => {
+ renderTray({ filters: { ...defaultFilterState, timeCGMUsePercent: '>=0.7' } });
+
+ expect(screen.getByText('≥ 70% CGM use')).toBeInTheDocument();
+ });
+ });
+
+ describe('tag chips', () => {
+ it('renders a "tagged" prefix and the tag name for an applied patient tag', () => {
+ renderTray({ filters: { ...defaultFilterState, patientTags: ['tag1'] } });
+
+ expect(screen.getByText('tagged')).toBeInTheDocument();
+ expect(screen.getByText('Tag One')).toBeInTheDocument();
+ });
+ });
+
+ describe('site chips', () => {
+ it('renders a "visiting" prefix and the site name for an applied clinic site', () => {
+ renderTray({ filters: { ...defaultFilterState, clinicSites: ['site1'] } });
+
+ expect(screen.getByText('visiting')).toBeInTheDocument();
+ expect(screen.getByText('Site Alpha')).toBeInTheDocument();
+ });
+ });
+
+ describe('removing a chip', () => {
+ it('fires onRemoveFilter with the chip type and value when its remove icon is clicked', async () => {
+ const { onRemoveFilter } = renderTray({
+ filters: { ...defaultFilterState, clinicSites: ['site1'] },
+ });
+
+ await userEvent.click(screen.getByLabelText('Remove Site Alpha filter'));
+
+ expect(onRemoveFilter).toHaveBeenCalledTimes(1);
+ expect(onRemoveFilter).toHaveBeenCalledWith('clinicSites', 'site1');
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/CGMUseFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/CGMUseFilterDropdown.test.js
new file mode 100644
index 0000000000..dd66c5a5ed
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/components/CGMUseFilterDropdown.test.js
@@ -0,0 +1,109 @@
+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 CGMUseFilterDropdown from '@app/pages/clinicworkspace/components/CGMUseFilterDropdown';
+import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
+
+const mockStore = configureStore([thunk]);
+
+describe('CGMUseFilterDropdown', () => {
+ let store;
+
+ const selectedClinicId = 'clinic123';
+
+ let onChange = jest.fn();
+
+ const ui = (props = {}) => (
+
+
+
+
+
+ );
+
+ const renderComponent = (props = {}) => render(ui(props));
+
+ beforeEach(() => {
+ store = mockStore({
+ blip: {
+ selectedClinicId,
+ clinics: { [selectedClinicId]: { id: selectedClinicId } },
+ },
+ });
+
+ onChange.mockClear();
+ mockTrackMetric.mockClear();
+ });
+
+ describe('filtering for cgm use', () => {
+ it('applies the cgm use filter based on the radio selected', async () => {
+ renderComponent({ timeCGMUsePercent: null });
+
+ // Dropdown closed initially
+ expect(screen.queryByTestId('cgm-use-filter-dropdown')).not.toBeInTheDocument();
+
+ // Open the dropdown
+ await userEvent.click(screen.getByRole('button', { name: /CGM Use/ }));
+ expect(screen.getByTestId('cgm-use-filter-dropdown')).toBeInTheDocument();
+
+ // Nothing selected initially
+ expect(screen.getByRole('radio', { name: /Less than 70%/ })).not.toBeChecked();
+ expect(screen.getByRole('radio', { name: /70% or more/ })).not.toBeChecked();
+
+ // Selecting an option and applying sets the filter
+ await userEvent.click(screen.getByRole('radio', { name: /Less than 70%/ }));
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(onChange).toHaveBeenCalledWith('<0.7');
+ expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - CGM use apply filter', {
+ clinicId: 'clinic123',
+ filter: '<0.7',
+ pageName: 'Population Health',
+ });
+
+ // Dropdown should automatically close
+ expect(screen.queryByTestId('cgm-use-filter-dropdown')).not.toBeInTheDocument();
+ });
+
+ it('disables the Apply button until an option is selected', async () => {
+ renderComponent({ timeCGMUsePercent: null });
+
+ await userEvent.click(screen.getByRole('button', { name: /CGM Use/ }));
+
+ // Disabled with nothing selected
+ expect(screen.getByRole('button', { name: /Apply/ })).toBeDisabled();
+
+ // Enabled once an option is selected
+ await userEvent.click(screen.getByRole('radio', { name: /70% or more/ }));
+ expect(screen.getByRole('button', { name: /Apply/ })).toBeEnabled();
+ });
+
+ it('pre-selects the radio matching the active filter', async () => {
+ renderComponent({ timeCGMUsePercent: '>=0.7' });
+
+ await userEvent.click(screen.getByRole('button', { name: /CGM Use/ }));
+
+ expect(screen.getByRole('radio', { name: /70% or more/ })).toBeChecked();
+ expect(screen.getByRole('radio', { name: /Less than 70%/ })).not.toBeChecked();
+ });
+
+ it('clears the filter', async () => {
+ renderComponent({ timeCGMUsePercent: '<0.7' });
+
+ await userEvent.click(screen.getByRole('button', { name: /CGM Use/ }));
+ expect(screen.getByRole('radio', { name: /Less than 70%/ })).toBeChecked();
+
+ await userEvent.click(screen.getByRole('button', { name: /Clear/ }));
+ expect(onChange).toHaveBeenCalledWith(null);
+ expect(screen.queryByTestId('cgm-use-filter-dropdown')).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js
new file mode 100644
index 0000000000..fa1aae7869
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js
@@ -0,0 +1,123 @@
+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 DataRecencyFilterDropdown from '@app/pages/clinicworkspace/components/DataRecencyFilterDropdown';
+import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
+
+const mockStore = configureStore([thunk]);
+
+describe('DataRecencyFilterDropdown', () => {
+ let store;
+
+ 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 = {}) => (
+
+
+
+
+
+ );
+
+ 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();
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/SiteFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/SiteFilterDropdown.test.js
new file mode 100644
index 0000000000..a43191333b
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/components/SiteFilterDropdown.test.js
@@ -0,0 +1,157 @@
+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 SiteFilterDropdown from '@app/pages/clinicworkspace/components/SiteFilterDropdown';
+import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
+import useIsClinicAdmin from '@app/pages/clinicworkspace/useIsClinicAdmin';
+import { SPECIAL_FILTER_STATES } from '@app/pages/clinicworkspace/useClinicPatientsFilters';
+
+jest.mock('@app/pages/clinicworkspace/useIsClinicAdmin');
+
+const mockStore = configureStore([thunk]);
+
+describe('SiteFilterDropdown', () => {
+ let store;
+
+ const selectedClinicId = 'clinic123';
+
+ const clinicSiteDefs = [
+ { id: 'site1', name: 'North Site' },
+ { id: 'site2', name: 'South Site' },
+ { id: 'site3', name: 'East Site' },
+ { id: 'site4', name: 'Downtown Clinic' },
+ ];
+
+ let onChange = jest.fn();
+ let onClickEditSites = jest.fn();
+ let clinicSites = [];
+
+ useIsClinicAdmin.mockReturnValue(true);
+
+ const ui = (props = {}) => (
+
+
+
+
+
+ );
+
+ const renderComponent = (props = {}) => render(ui(props));
+
+ beforeEach(() => {
+ store = mockStore({
+ blip: {
+ selectedClinicId,
+ clinics: { [selectedClinicId]: { id: selectedClinicId, sites: clinicSiteDefs } },
+ },
+ });
+
+ onChange.mockClear();
+ onClickEditSites.mockClear();
+ mockTrackMetric.mockClear();
+ });
+
+ describe('filtering for sites', () => {
+ it('applies sites based on checkboxes selected', async () => {
+ renderComponent({ clinicSites: ['site1', 'site3'] });
+
+ // Dropdown closed initially
+ expect(screen.queryByTestId('site-filter-dropdown')).not.toBeInTheDocument();
+
+ // Open the dropdown
+ await userEvent.click(screen.getByRole('button', { name: /Clinic Sites/ }));
+ expect(screen.getByTestId('site-filter-dropdown')).toBeInTheDocument();
+
+ expect(screen.getByRole('checkbox', { name: /North Site/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /South Site/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /East Site/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Downtown Clinic/ })).not.toBeChecked();
+
+ // Typing into the box should search down the options
+ await userEvent.click(screen.getByRole('textbox'));
+ await userEvent.paste(' site');
+ expect(screen.getByRole('checkbox', { name: /North Site/ })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: /South Site/ })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: /East Site/ })).toBeInTheDocument();
+ expect(screen.queryByRole('checkbox', { name: /Downtown Clinic/ })).not.toBeInTheDocument();
+
+ // Applying a checkbox filter sets the filter
+ await userEvent.click(screen.getByRole('checkbox', { name: /North Site/ })); // unselect
+ await userEvent.click(screen.getByRole('checkbox', { name: /South Site/ })); // select
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(onChange).toHaveBeenCalledWith(['site3', 'site2']);
+ expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Clinic sites filter apply', { clinicId: 'clinic123', pageName: 'Population Health' })
+
+ // Dropdown should automatically close
+ expect(screen.queryByTestId('site-filter-dropdown')).not.toBeInTheDocument();
+ });
+
+ it('applies a special state for patients without sites', async () => {
+ renderComponent({ clinicSites: ['site1', 'site3'] });
+
+ await userEvent.click(screen.getByRole('button', { name: /Clinic Sites/ }));
+ expect(screen.getByRole('checkbox', { name: /North Site/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /South Site/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /East Site/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Downtown Clinic/ })).not.toBeChecked();
+
+ // Clicking the Patients without any sites checkbox should uncheck all
+ await userEvent.click(screen.getByRole('checkbox', { name: /Patients without any sites/ }));
+ expect(screen.getByRole('checkbox', { name: /North Site/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /South Site/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /East Site/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Downtown Clinic/ })).not.toBeChecked();
+
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(onChange).toHaveBeenCalledWith(SPECIAL_FILTER_STATES.ZERO_SITES);
+ });
+
+ it('clears the filter', async () => {
+ renderComponent({ clinicSites: ['site1', 'site3'] });
+
+ await userEvent.click(screen.getByRole('button', { name: /Clinic Sites/ }));
+ expect(screen.getByRole('checkbox', { name: /North Site/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /South Site/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /East Site/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Downtown Clinic/ })).not.toBeChecked();
+
+ await userEvent.click(screen.getByRole('button', { name: /Clear/ }));
+ expect(onChange).toHaveBeenCalledWith([]);
+ expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Clinic sites filter clear', { clinicId: 'clinic123', pageName: 'Population Health' })
+ expect(screen.queryByTestId('site-filter-dropdown')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('edit sites', () => {
+ it('conditionally renders a button to edit sites', async () => {
+ // Should be hidden if no passed callback fn
+ useIsClinicAdmin.mockReturnValue(true);
+ const { rerender } = renderComponent({ onClickEditSites: null });
+ await userEvent.click(screen.getByRole('button', { name: /Clinic Sites/ }));
+
+ expect(screen.queryByLabelText(/Edit Sites/)).not.toBeInTheDocument();
+
+ // Should be hidden if not Clinic Admin
+ useIsClinicAdmin.mockReturnValue(false);
+ rerender(ui());
+ expect(screen.queryByLabelText(/Edit Sites/)).not.toBeInTheDocument();
+
+ // Visible if Clinic Admin and passed callback fn
+ useIsClinicAdmin.mockReturnValue(true);
+ rerender(ui());
+
+ await userEvent.click(screen.getByLabelText(/Edit Sites/));
+ expect(onClickEditSites).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown.test.js
new file mode 100644
index 0000000000..9c6946fa7b
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown.test.js
@@ -0,0 +1,113 @@
+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 SummaryPeriodFilterDropdown from '@app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown';
+import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
+
+const mockStore = configureStore([thunk]);
+
+describe('SummaryPeriodFilterDropdown', () => {
+ let store;
+
+ const selectedClinicId = 'clinic123';
+
+ let onChange = jest.fn();
+
+ const ui = (props = {}) => (
+
+
+
+
+
+ );
+
+ const renderComponent = (props = {}) => render(ui(props));
+
+ beforeEach(() => {
+ store = mockStore({
+ blip: {
+ selectedClinicId,
+ clinics: { [selectedClinicId]: { id: selectedClinicId } },
+ },
+ });
+
+ onChange.mockClear();
+ mockTrackMetric.mockClear();
+ });
+
+ describe('filtering for summary period', () => {
+ it('applies the summary period based on the radio selected', async () => {
+ renderComponent({ activeSummaryPeriod: '14d' });
+
+ // Should have correct label
+ expect(screen.getByRole('button', { name: /Summarizing 14 days of data/ })).toBeInTheDocument();
+
+ // Dropdown closed initially
+ expect(screen.queryByTestId('summary-period-filter-dropdown')).not.toBeInTheDocument();
+
+ // Open the dropdown
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+ expect(screen.getByRole('radio', { name: /24 hours/ })).toBeInTheDocument();
+ expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Summary period filter open', { clinicId: 'clinic123', pageName: 'Population Health' });
+
+ // The active period is pre-selected
+ expect(screen.getByRole('radio', { name: /14 days/ })).toBeChecked();
+ expect(screen.getByRole('radio', { name: /30 days/ })).not.toBeChecked();
+
+ // Selecting a different period and applying sets the filter
+ await userEvent.click(screen.getByRole('radio', { name: /30 days/ }));
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(onChange).toHaveBeenCalledWith('30d');
+ expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Summary period apply filter', { clinicId: 'clinic123', summaryPeriod: '30d', pageName: 'Population Health' });
+
+ // Dropdown should automatically close
+ expect(screen.queryByTestId('summary-period-filter-dropdown')).not.toBeInTheDocument();
+ });
+
+ it('disables the Apply button until a different period is selected', async () => {
+ renderComponent({ activeSummaryPeriod: '14d' });
+
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+
+ // Disabled while the pending selection matches the active period
+ expect(screen.getByRole('button', { name: /Apply/ })).toBeDisabled();
+
+ // Enabled once a different period is selected
+ await userEvent.click(screen.getByRole('radio', { name: /30 days/ }));
+ expect(screen.getByRole('button', { name: /Apply/ })).toBeEnabled();
+
+ // Disabled again when re-selecting the active period
+ await userEvent.click(screen.getByRole('radio', { name: /14 days/ }));
+ expect(screen.getByRole('button', { name: /Apply/ })).toBeDisabled();
+ });
+
+ it('cancels without applying and resets the pending selection', async () => {
+ renderComponent({ activeSummaryPeriod: '14d' });
+
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+
+ // Change the selection, then cancel
+ await userEvent.click(screen.getByRole('radio', { name: /30 days/ }));
+ await userEvent.click(screen.getByRole('button', { name: /Cancel/ }));
+
+ // No change is applied and the dropdown closes
+ expect(onChange).not.toHaveBeenCalled();
+ expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Summary period filter cancel', { clinicId: 'clinic123', pageName: 'Population Health' });
+ expect(screen.queryByTestId('summary-period-filter-dropdown')).not.toBeInTheDocument();
+
+ // Re-opening shows the original active period still selected (pending was reset)
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+ expect(screen.getByRole('radio', { name: /14 days/ })).toBeChecked();
+ expect(screen.getByRole('radio', { name: /30 days/ })).not.toBeChecked();
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/TagFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/TagFilterDropdown.test.js
new file mode 100644
index 0000000000..604e4c6224
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/components/TagFilterDropdown.test.js
@@ -0,0 +1,157 @@
+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 TagFilterDropdown from '@app/pages/clinicworkspace/components/TagFilterDropdown';
+import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
+import useIsClinicAdmin from '@app/pages/clinicworkspace/useIsClinicAdmin';
+import { SPECIAL_FILTER_STATES } from '@app/pages/clinicworkspace/useClinicPatientsFilters';
+
+jest.mock('@app/pages/clinicworkspace/useIsClinicAdmin');
+
+const mockStore = configureStore([thunk]);
+
+describe('TagFilterDropdown', () => {
+ let store;
+
+ const selectedClinicId = 'clinic123';
+
+ const patientTagDefs = [
+ { id: 'tag1', name: 'Week 1' },
+ { id: 'tag2', name: 'Week 2' },
+ { id: 'tag3', name: 'Week 3' },
+ { id: 'tag4', name: 'Pregnancy' },
+ ];
+
+ let onChange = jest.fn();
+ let onClickEditTags = jest.fn();
+ let patientTags = [];
+
+ useIsClinicAdmin.mockReturnValue(true);
+
+ const ui = (props = {}) => (
+
+
+
+
+
+ );
+
+ const renderComponent = (props = {}) => render(ui(props));
+
+ beforeEach(() => {
+ store = mockStore({
+ blip: {
+ selectedClinicId,
+ clinics: { [selectedClinicId]: { id: selectedClinicId, patientTags: patientTagDefs } },
+ },
+ });
+
+ onChange.mockClear();
+ onClickEditTags.mockClear();
+ mockTrackMetric.mockClear();
+ });
+
+ describe('filtering for tags', () => {
+ it('applies tags based on checkboxes selected', async () => {
+ renderComponent({ patientTags: ['tag1', 'tag3'] });
+
+ // Dropdown closed initially
+ expect(screen.queryByTestId('tag-filter-dropdown')).not.toBeInTheDocument();
+
+ // Open the dropdown
+ await userEvent.click(screen.getByRole('button', { name: /Tags/ }));
+ expect(screen.getByTestId('tag-filter-dropdown')).toBeInTheDocument();
+
+ expect(screen.getByRole('checkbox', { name: /Week 1/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Week 2/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Week 3/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Pregnancy/ })).not.toBeChecked();
+
+ // Typing into the box should search down the options
+ await userEvent.click(screen.getByRole('textbox'));
+ await userEvent.paste(' wee');
+ expect(screen.getByRole('checkbox', { name: /Week 1/ })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: /Week 2/ })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: /Week 3/ })).toBeInTheDocument();
+ expect(screen.queryByRole('checkbox', { name: /Pregnancy/ })).not.toBeInTheDocument();
+
+ // Applying a checkbox filter sets the filter
+ await userEvent.click(screen.getByRole('checkbox', { name: /Week 1/ })); // unselect
+ await userEvent.click(screen.getByRole('checkbox', { name: /Week 2/ })); // select
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(onChange).toHaveBeenCalledWith(['tag3', 'tag2']);
+ expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Patient tag filter apply', { clinicId: 'clinic123', pageName: 'Population Health' })
+
+ // Dropdown should automatically close
+ expect(screen.queryByTestId('tag-filter-dropdown')).not.toBeInTheDocument();
+ });
+
+ it('applies a special state for patients without tags', async () => {
+ renderComponent({ patientTags: ['tag1', 'tag3'] });
+
+ await userEvent.click(screen.getByRole('button', { name: /Tags/ }));
+ expect(screen.getByRole('checkbox', { name: /Week 1/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Week 2/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Week 3/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Pregnancy/ })).not.toBeChecked();
+
+ // Clicking the Patients without any tags checkbox should uncheck all
+ await userEvent.click(screen.getByRole('checkbox', { name: /Patients without any tags/ }));
+ expect(screen.getByRole('checkbox', { name: /Week 1/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Week 2/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Week 3/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Pregnancy/ })).not.toBeChecked();
+
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(onChange).toHaveBeenCalledWith(SPECIAL_FILTER_STATES.ZERO_TAGS);
+ });
+
+ it('clears the filter', async () => {
+ renderComponent({ patientTags: ['tag1', 'tag3'] });
+
+ await userEvent.click(screen.getByRole('button', { name: /Tags/ }));
+ expect(screen.getByRole('checkbox', { name: /Week 1/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Week 2/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Week 3/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Pregnancy/ })).not.toBeChecked();
+
+ await userEvent.click(screen.getByRole('button', { name: /Clear/ }));
+ expect(onChange).toHaveBeenCalledWith([]);
+ expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Patient tag filter clear', { clinicId: 'clinic123', pageName: 'Population Health' });
+ expect(screen.queryByTestId('tag-filter-dropdown')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('edit tags', () => {
+ it('conditionally renders a button to edit tags', async () => {
+ // Should be hidden if no passed callback fn
+ useIsClinicAdmin.mockReturnValue(true);
+ const { rerender } = renderComponent({ onClickEditTags: null });
+ await userEvent.click(screen.getByRole('button', { name: /Tags/ }));
+
+ expect(screen.queryByLabelText(/Edit Tags/)).not.toBeInTheDocument();
+
+ // Should be hidden if not Clinic Admin
+ useIsClinicAdmin.mockReturnValue(false);
+ rerender(ui());
+ expect(screen.queryByLabelText(/Edit Tags/)).not.toBeInTheDocument();
+
+ // Visible if Clinic Admin and passed callback fn
+ useIsClinicAdmin.mockReturnValue(true);
+ rerender(ui());
+
+ await userEvent.click(screen.getByLabelText(/Edit Tags/));
+ expect(onClickEditTags).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/TimeInRangeFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/TimeInRangeFilterDropdown.test.js
new file mode 100644
index 0000000000..1c673bfa43
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/components/TimeInRangeFilterDropdown.test.js
@@ -0,0 +1,170 @@
+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 { useFlags } from 'launchdarkly-react-client-sdk';
+import TimeInRangeFilterDropdown from '@app/pages/clinicworkspace/components/TimeInRangeFilterDropdown';
+import { MMOLL_UNITS } from '@app/core/constants';
+import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
+
+jest.mock('launchdarkly-react-client-sdk');
+
+const mockStore = configureStore([thunk]);
+
+describe('TimeInRangeFilterDropdown', () => {
+ let store;
+
+ const selectedClinicId = 'clinic123';
+
+ let onChange = jest.fn();
+
+ const ui = (props = {}) => (
+
+
+
+
+
+ );
+
+ const renderComponent = (props = {}) => render(ui(props));
+
+ beforeEach(() => {
+ store = mockStore({
+ blip: {
+ selectedClinicId,
+ clinics: { [selectedClinicId]: { id: selectedClinicId } },
+ },
+ });
+
+ useFlags.mockReturnValue({ showExtremeHigh: false });
+ onChange.mockClear();
+ mockTrackMetric.mockClear();
+ });
+
+ describe('filtering for time in range', () => {
+ it('applies the time in range filters based on checkboxes selected', async () => {
+ renderComponent({ timeInRange: [] });
+
+ // Empty due to no TIR filters applied
+ expect(screen.queryByLabelText('filter count')).not.toBeInTheDocument();
+
+ // Dropdown closed initially
+ expect(screen.queryByTestId('time-in-range-filter-dropdown')).not.toBeInTheDocument();
+
+ // Open the dropdown
+ await userEvent.click(screen.getByRole('button', { name: /Time in Range/ }));
+ expect(screen.getByRole('checkbox', { name: /Very High/ })).toBeInTheDocument();
+
+ // Nothing selected initially
+ expect(screen.getByRole('checkbox', { name: /Very High/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /^High/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Not meeting TIR/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /^Low/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Very Low/ })).not.toBeChecked();
+
+ // Selecting ranges and applying sets the filter
+ await userEvent.click(screen.getByRole('checkbox', { name: /Very High/ }));
+ await userEvent.click(screen.getByRole('checkbox', { name: /Very Low/ }));
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(onChange).toHaveBeenCalledWith(['timeInVeryHighPercent', 'timeInVeryLowPercent']);
+
+ // Dropdown should automatically close
+ expect(screen.queryByTestId('time-in-range-filter-dropdown')).not.toBeInTheDocument();
+ });
+
+ it('clears the filter', async () => {
+ renderComponent({ timeInRange: ['timeInVeryHighPercent', 'timeInVeryLowPercent'] });
+
+ expect(screen.getByLabelText('filter count')).toHaveTextContent('2');
+
+ await userEvent.click(screen.getByRole('button', { name: /Time in Range/ }));
+ expect(screen.getByRole('checkbox', { name: /Very High/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /^High/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Not meeting TIR/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /^Low/ })).not.toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Very Low/ })).toBeChecked();
+
+ await userEvent.click(screen.getByRole('button', { name: /Clear/ }));
+ expect(onChange).toHaveBeenCalledWith([]);
+ expect(screen.queryByRole('checkbox', { name: /Very High/ })).not.toBeInTheDocument();
+ });
+
+ it('shows the highest range option only when the showExtremeHigh flag is set', async () => {
+ // Hidden when the flag is off
+ const { rerender } = renderComponent();
+ await userEvent.click(screen.getByRole('button', { name: /Time in Range/ }));
+ expect(screen.queryByRole('checkbox', { name: /Extremely High/ })).not.toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: /Very High/ })).toBeInTheDocument();
+
+ // Visible when the flag is on
+ useFlags.mockReturnValue({ showExtremeHigh: true });
+ rerender(ui());
+ expect(screen.getByRole('checkbox', { name: /Extremely High/ })).toBeInTheDocument();
+ });
+
+ it('removes a range when its checkbox is unchecked', async () => {
+ renderComponent({ timeInRange: ['timeInVeryHighPercent', 'timeInVeryLowPercent'] });
+
+ await userEvent.click(screen.getByRole('button', { name: /Time in Range/ }));
+ expect(screen.getByRole('checkbox', { name: /Very High/ })).toBeChecked();
+ expect(screen.getByRole('checkbox', { name: /Very Low/ })).toBeChecked();
+
+ // Unchecking a selected range removes it from the applied filter
+ await userEvent.click(screen.getByRole('checkbox', { name: /Very High/ }));
+ expect(screen.getByRole('checkbox', { name: /Very High/ })).not.toBeChecked();
+
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(onChange).toHaveBeenCalledWith(['timeInVeryLowPercent']);
+ });
+
+ it('applies the "Not meeting TIR" (target) range', async () => {
+ renderComponent({ timeInRange: [] });
+
+ await userEvent.click(screen.getByRole('button', { name: /Time in Range/ }));
+
+ // This is the only option using a "Less than" threshold (the rest use "Greater than")
+ expect(screen.getByText(/Less than 70%/)).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('checkbox', { name: /Not meeting TIR/ }));
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(onChange).toHaveBeenCalledWith(['timeInTargetPercent']);
+ });
+
+ it('renders the range definitions in mg/dL units', async () => {
+ renderComponent({ timeInRange: [] });
+ await userEvent.click(screen.getByRole('button', { name: /Time in Range/ }));
+
+ expect(screen.getByText('Greater than 1% Time <54 mg/dL')).toBeInTheDocument(); // Very Low
+ expect(screen.getByText('Greater than 4% Time <70 mg/dL')).toBeInTheDocument(); // Low
+ expect(screen.getByText('Less than 70% Time between 70-180 mg/dL')).toBeInTheDocument(); // Not meeting TIR
+ expect(screen.getByText('Greater than 25% Time >180 mg/dL')).toBeInTheDocument(); // High
+ expect(screen.getByText('Greater than 5% Time >250 mg/dL')).toBeInTheDocument(); // Very High
+ });
+
+ it('renders the range definitions in mmol/L units', async () => {
+ store = mockStore({
+ blip: {
+ selectedClinicId,
+ clinics: { [selectedClinicId]: { id: selectedClinicId, preferredBgUnits: MMOLL_UNITS } },
+ },
+ });
+
+ renderComponent({ timeInRange: [] });
+ await userEvent.click(screen.getByRole('button', { name: /Time in Range/ }));
+
+ expect(screen.getByText('Greater than 1% Time <3.0 mmol/L')).toBeInTheDocument(); // Very Low
+ expect(screen.getByText('Greater than 4% Time <3.9 mmol/L')).toBeInTheDocument(); // Low
+ expect(screen.getByText('Less than 70% Time between 3.9-10.0 mmol/L')).toBeInTheDocument(); // Not meeting TIR
+ expect(screen.getByText('Greater than 25% Time >10.0 mmol/L')).toBeInTheDocument(); // High
+ expect(screen.getByText('Greater than 5% Time >13.9 mmol/L')).toBeInTheDocument(); // Very High
+ });
+ });
+});
diff --git a/app/core/clinicUtils.js b/app/core/clinicUtils.js
index ee925e273b..fddfcc4f68 100644
--- a/app/core/clinicUtils.js
+++ b/app/core/clinicUtils.js
@@ -87,6 +87,17 @@ export const summaryPeriodOptions = [
{ value: '30d', label: t('30 days') },
];
+export const timeInRangeFilterThresholds = {
+ timeInVeryLowPercent: { value: 1, comparator: '>' },
+ timeInLowPercent: { value: 4, comparator: '>' },
+ timeInAnyLowPercent: { value: 4, comparator: '>' },
+ timeInTargetPercent: { value: 70, comparator: '<' },
+ timeInHighPercent: { value: 25, comparator: '>' },
+ timeInAnyHighPercent: { value: 25, comparator: '>' },
+ timeInVeryHighPercent: { value: 5, comparator: '>' },
+ timeInExtremeHighPercent: { value: 1, comparator: '>' },
+};
+
export const timezoneOptions = map(
timezoneNames,
name => ({ value: name, label: name })
diff --git a/app/core/icons/tagIcon.svg b/app/core/icons/tagIcon.svg
new file mode 100644
index 0000000000..eb0439f030
--- /dev/null
+++ b/app/core/icons/tagIcon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/pages/clinicadmin/clinicadmin.js b/app/pages/clinicadmin/clinicadmin.js
index f9da0080eb..f0e1fbc9a3 100644
--- a/app/pages/clinicadmin/clinicadmin.js
+++ b/app/pages/clinicadmin/clinicadmin.js
@@ -5,7 +5,6 @@ import { useTranslation, Trans } from 'react-i18next';
import { push } from 'connected-react-router';
import compact from 'lodash/compact';
import filter from 'lodash/filter';
-import find from 'lodash/find';
import get from 'lodash/get'
import has from 'lodash/has';
import includes from 'lodash/includes';
@@ -25,7 +24,6 @@ import { useFormik } from 'formik';
import { useFlags } from 'launchdarkly-react-client-sdk';
import {
- Title,
MediumTitle,
Body1,
} from '../../components/elements/FontStyles';
@@ -63,6 +61,7 @@ import {
import config from '../../config';
import Icon from '../../components/elements/Icon';
import utils from '../../core/utils';
+import useIsClinicAdmin from '../clinicworkspace/useIsClinicAdmin';
const clinicTypesLabels = mapValues(keyBy(clinicTypes, 'value'), 'label');
@@ -93,7 +92,6 @@ export const ClinicAdmin = (props) => {
const pendingSentClinicianInvites = useSelector((state) => state.blip.pendingSentClinicianInvites);
const timePrefs = useSelector((state) => state.blip.timePrefs);
const [clinicianArray, setClinicianArray] = useState([]);
- const [userRolesInClinic, setUserRolesInClinic] = useState([]);
const [sortOptions, setSortOptions] = useState({ orderBy: 'fullName', order: 'asc' });
const sortedClinicianArray = useMemo(() => {
@@ -127,7 +125,7 @@ export const ClinicAdmin = (props) => {
validationSchema,
});
- const isClinicAdmin = () => includes(userRolesInClinic, 'CLINIC_ADMIN');
+ const isClinicAdmin = useIsClinicAdmin();
const isOnlyClinicAdmin = () => filter(clinicianArray, { isAdmin: true, inviteId: undefined }).length === 1;
useEffect(() => {
@@ -316,7 +314,6 @@ export const ClinicAdmin = (props) => {
}, [clinic?.clinicians]);
useEffect(() => {
- setUserRolesInClinic(get(find(clinicianArray, { userId: loggedInUserId }), 'roles', []));
setPageCount(Math.ceil(clinicianArray.length / rowsPerPage));
}, [clinicianArray]);
@@ -659,7 +656,7 @@ export const ClinicAdmin = (props) => {
render: renderRole,
});
- if (isClinicAdmin()) {
+ if (isClinicAdmin) {
columns.push({
title: t('Security'),
field: 'mfaEnabled',
@@ -670,7 +667,7 @@ export const ClinicAdmin = (props) => {
});
}
- if (((isClinicAdmin()))) {
+ if (isClinicAdmin) {
columns.push(
{
title: '',
@@ -724,7 +721,7 @@ export const ClinicAdmin = (props) => {
- {isClinicAdmin() && (
+ {isClinicAdmin && (