Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
737e8cf
WEB-4460 initialize base branch
henry-tp Aug 18, 2026
a8836e2
WEB-4460 add new adapter components to dashboard
henry-tp Aug 17, 2026
6fd806b
WEB-4460 remove DataRecencyType from Data Recency dropdown via prop
henry-tp Aug 17, 2026
d3b9476
WEB-4460 add 14 day Data Recency option in TIDE
henry-tp Aug 20, 2026
7d19686
WEB-4460 alter styling for non-removable filters
henry-tp Aug 20, 2026
f6ddee5
WEB-4460 add TIDE to metric source
henry-tp Aug 20, 2026
7fab846
Revert "WEB-4460 delete work related to filter persistence to move it…
henry-tp Aug 20, 2026
869f49a
WEB-4460 fix rebase errors
henry-tp Aug 20, 2026
543820a
WEB-4460 fix useDerivedDataRecency hook to use redux
henry-tp Aug 24, 2026
cebc9c2
WEB-4460 add required filters to ActiveFilterTray
henry-tp Aug 24, 2026
58b3aad
WEB-4460 add tests for filters and emptycontent
henry-tp Aug 24, 2026
0211602
WEB-4460 add tests for usePruneInvalidFilters
henry-tp Aug 24, 2026
b6107b0
WEB-4460 migrate tests for async actions
henry-tp Aug 25, 2026
55247b5
WEB-4460 add tests for localStorage persistence from configureStore
henry-tp Aug 25, 2026
314f3ff
WEB-4460 use more conventional style of redux action in tests
henry-tp Aug 25, 2026
c07e222
WEB-4460 add test for DEFAULT_WITH_FILTERS
henry-tp Aug 27, 2026
e217978
WEB-4460 re-run prune hook on clinic change
henry-tp Aug 28, 2026
704f40c
WEB-4460 move filters into directory
henry-tp Aug 31, 2026
a8ecf23
WEB-4460 organize filters into specific directory
henry-tp Aug 31, 2026
74dc5ed
WEB-4460 group requiredFilters for clarity
henry-tp Sep 1, 2026
9540776
WEB-4460 address automated comment feedback
henry-tp Sep 4, 2026
0b9cc4a
WEB-4460 use atomic state setting for tideDashboardFilters
henry-tp Sep 4, 2026
3cb89eb
WEB-4460 remove dup import
henry-tp Sep 6, 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
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import isEqual from 'lodash/isEqual';
Expand All @@ -11,7 +12,6 @@ import { setupStore } from '@tests/utils/setupStore';
import blipReducer from '@app/redux/reducers';
import { CATEGORY } from '@app/pages/clinicworkspace/TideDashboardV2/tideDashboardSlice';
import TideDashboardV2 from '@app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2';
import { MemoryRouter } from 'react-router-dom';

// Pin the data recency window to a stable [lastDataFrom, lastDataTo]
jest.mock('@app/pages/clinicworkspace/TideDashboardV2/useDerivedDataRecencyEndpoints', () => ({
Expand Down Expand Up @@ -119,9 +119,18 @@ const anticipatedQueries = {
'cgm.timeInVeryHighPercent': '<0.045',
'cgm.timeCGMUsePercent': '>=0.695',
},
'DEFAULT_WITH_FILTERS': {
offset: '0',
limit: '12',
period: '30d',
'cgm.lastDataFrom': '2025-05-23T00:00:00.000Z',
'cgm.lastDataTo': '2025-05-30T00:00:00.000Z',
tags: 'tag8',
sites: 'site9',
},
};

const patientsForCategory = {
const datasets = {
[DEFAULT]: [
{ id: 'default-1', fullName: 'Default Patient 1', birthDate: '2001-01-01' },
{ id: 'default-2', fullName: 'Default Patient 2', birthDate: '2002-02-02' },
Expand Down Expand Up @@ -154,22 +163,25 @@ const patientsForCategory = {
{ id: 'target-1', fullName: 'Meeting Targets Patient 1', birthDate: '2015-03-15' },
{ id: 'target-2', fullName: 'Meeting Targets Patient 2', birthDate: '2016-04-16' },
],
'DEFAULT_WITH_FILTERS': [
{ id: 'filtered-3', fullName: 'Filtered Patient 3', birthDate: '2001-01-01' },
],
};

const getCorrespondingDataForQuery = (searchParams) => {
let datasetName;
let matchedDatasetName;

// Look over anticipated queries. If the query matches, return the dataset
for (let [category, anticipatedQuery] of entries(anticipatedQueries)) {
for (let [datasetName, anticipatedQuery] of entries(anticipatedQueries)) {
if (isEqual(searchParams, anticipatedQuery)) {
datasetName = category;
matchedDatasetName = datasetName;
break;
}
}

if (!datasetName) throw new Error('No data for this query found');
if (!matchedDatasetName) throw new Error('No data for this query found');

return patientsForCategory[datasetName];
return datasets[matchedDatasetName];
};

const server = setupServer(
Expand Down Expand Up @@ -263,4 +275,64 @@ describe('TideDashboardV2', () => {
expect(screen.getByText('Meeting Targets Patient 2')).toBeInTheDocument();
expect(screen.queryByText('Low CGM Wear Patient 1')).not.toBeInTheDocument();
}, TEST_TIMEOUT_MS);

it('fetches with filters', async () => {
store = setupStore({
blip: {
selectedClinicId: 'clinic123',
clinics: { clinic123: { id: 'clinic123', patientTags: [{ id: 'tag8', name: 'Tag 8' }], sites: [{ id: 'site9', name: 'Site 9' }] } },
tideDashboardFilters: { lastData: 7, patientTags: ['tag8'], clinicSites: ['site9'], summaryPeriod: '30d' },
},
}, { blip: blipReducer });

renderComponent();

expect(await screen.findByText('Filtered Patient 3')).toBeInTheDocument();
}, TEST_TIMEOUT_MS);

describe('empty content', () => {
beforeEach(() => {
let fetchCount = 0;

server.use(
http.get('http://app.tidepool.test/v1/clinics/clinic123/patients', () => {
fetchCount += 1;

return fetchCount <= 1
// first fetch -> no patients
? HttpResponse.json({ data: [], meta: { count: 0 } })
// refetch -> 1 patient
: HttpResponse.json({ data: [{ id: 'default-1', fullName: 'Default Patient 1', birthDate: '2001-01-01' }], meta: { count: 1 } });
}
)
);
});

it('shows Reset button when no patients match applied filters', async () => {
// A tag filter is applied before the dashboard mounts
store = setupStore({
blip: {
selectedClinicId: 'clinic123',
clinics: { clinic123: { id: 'clinic123', patientTags: [{ id: 'tag1', name: 'Tag One' }] } },
tideDashboardFilters: { lastData: 7, patientTags: ['tag1'], clinicSites: [], summaryPeriod: '14d' },
},
}, { blip: blipReducer });

renderComponent();

const emptyContent = await screen.findByTestId('tide-dashboard-empty-content');
expect(within(emptyContent).getByText('There are no patients with the current filter(s)')).toBeInTheDocument();
await userEvent.click(within(emptyContent).getByRole('button', { name: 'Reset All Filters' }));

expect(await screen.findByText('Default Patient 1')).toBeInTheDocument();
});

it('shows an empty message without a reset button when there are no patients and no filters applied', async () => {
renderComponent();

const emptyContent = await screen.findByTestId('tide-dashboard-empty-content');
expect(within(emptyContent).getByText('There are no results to show')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Reset All Filters' })).not.toBeInTheDocument(); // no filters = no button
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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/TideDashboardV2/filters/FilterByDataRecency';

const mockStore = configureStore([thunk]);

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

const renderComponent = () => render(
<Provider store={store}>
<MemoryRouter initialEntries={['/clinic-workspace/tide-dashboard']}>
<FilterByDataRecency />
</MemoryRouter>
</Provider>
);

it('dispatches the lastData filter and offset reset when data recency filter is applied', async () => {
store = mockStore({
blip: {
selectedClinicId: 'clinic123',
tideDashboardFilters: {
lastData: 7,
patientTags: [],
clinicSites: [],
summaryPeriod: '14d',
},
},
});

renderComponent();

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

expect(screen.queryByRole('radio', { name: /CGM/ })).not.toBeInTheDocument(); // not selectable in TIDE
expect(screen.queryByRole('radio', { name: /BGM/ })).not.toBeInTheDocument(); // not selectable in TIDE

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

// Select an option
await userEvent.click(screen.getByRole('radio', { name: /Within 2 days/ }));
expect(store.getActions()).toStrictEqual([]);

// Applying the filter dispatches the new filter value and resets the page offset
await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
expect(store.getActions()).toStrictEqual([
{ type: 'tideDashboardFilters/setLastDataFilter', payload: 2 },
{ type: 'tideDashboard/setOffset', payload: 0 },
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 FilterBySites from '@app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySites';

const mockStore = configureStore([thunk]);

describe('FilterBySites', () => {
let store;

const renderComponent = () => render(
<Provider store={store}>
<MemoryRouter initialEntries={['/clinic-workspace/tide-dashboard']}>
<FilterBySites />
</MemoryRouter>
</Provider>
);

it('dispatches the clinic sites filter and an offset reset when a site filter is applied', async () => {
store = mockStore({
blip: {
selectedClinicId: 'clinic123',
clinics: {
clinic123: {
id: 'clinic123',
sites: [{ id: 'site1', name: 'Site Alpha' }, { id: 'site2', name: 'Site Bravo' }],
},
},
tideDashboardFilters: {
lastData: 7,
patientTags: [],
clinicSites: ['site1'],
summaryPeriod: '14d',
},
},
});

renderComponent();

// Open the dropdown
expect(screen.queryByTestId('site-filter-dropdown')).not.toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: /Clinic Sites/ }));
expect(screen.getByTestId('site-filter-dropdown')).toBeInTheDocument();

// Selecting a site
await userEvent.click(screen.getByRole('checkbox', { name: /Site Bravo/ }));
expect(store.getActions()).toStrictEqual([]);

// Applying the filter dispatches the selected sites and resets the page offset
await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
expect(store.getActions()).toStrictEqual([
{ type: 'tideDashboardFilters/setClinicSitesFilter', payload: ['site1', 'site2'] },
{ type: 'tideDashboard/setOffset', payload: 0 },
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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/TideDashboardV2/filters/FilterBySummaryPeriod';

const mockStore = configureStore([thunk]);

describe('FilterBySummaryPeriod', () => {
let store;

const renderComponent = () => render(
<Provider store={store}>
<MemoryRouter initialEntries={['/clinic-workspace/tide-dashboard']}>
<FilterBySummaryPeriod />
</MemoryRouter>
</Provider>
);

it('dispatches the summary period filter and an offset reset when a summary period is applied', async () => {
store = mockStore({
blip: {
selectedClinicId: 'clinic123',
tideDashboardFilters: {
lastData: 7,
patientTags: [],
clinicSites: [],
summaryPeriod: '14d' },
},
});

renderComponent();

// Open the dropdown
expect(screen.queryByTestId('summary-period-filter-dropdown')).not.toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: /Summarizing 14 days of data/ }));

expect(screen.getByTestId('summary-period-filter-dropdown')).toBeInTheDocument();
expect(screen.getByRole('radio', { name: /24 hours/ })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: /7 days/ })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: /14 days/ })).toBeChecked();
expect(screen.getByRole('radio', { name: /30 days/ })).toBeInTheDocument();

// Selecting a period
await userEvent.click(screen.getByRole('radio', { name: /30 days/ }));
expect(store.getActions()).toStrictEqual([]);

// Applying the filter dispatches the new period and resets the page offset
await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
expect(store.getActions()).toStrictEqual([
{ type: 'tideDashboardFilters/setSummaryPeriodFilter', payload: '30d' },
{ type: 'tideDashboard/setOffset', payload: 0 },
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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 FilterByTags from '@app/pages/clinicworkspace/TideDashboardV2/filters/FilterByTags';

const mockStore = configureStore([thunk]);

describe('FilterByTags', () => {
let store;

const renderComponent = () => render(
<Provider store={store}>
<MemoryRouter initialEntries={['/clinic-workspace/tide-dashboard']}>
<FilterByTags />
</MemoryRouter>
</Provider>
);

it('dispatches the patient tags filter and an offset reset when a tag filter is applied', async () => {
store = mockStore({
blip: {
selectedClinicId: 'clinic123',
clinics: {
clinic123: {
id: 'clinic123',
patientTags: [
{ id: 'tag1', name: 'Week 1' },
{ id: 'tag2', name: 'Week 2' },
],
},
},
tideDashboardFilters: {
lastData: 7,
patientTags: [],
clinicSites: [],
summaryPeriod: '14d',
},
},
});

renderComponent();

// Open the dropdown
expect(screen.queryByTestId('tag-filter-dropdown')).not.toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: /Tags/ }));
expect(screen.getByTestId('tag-filter-dropdown')).toBeInTheDocument();

// Selecting a tag
await userEvent.click(screen.getByRole('checkbox', { name: /Week 2/ }));
expect(store.getActions()).toStrictEqual([]);

// Applying the filter dispatches the selected tags and resets the page offset
await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
expect(store.getActions()).toStrictEqual([
{ type: 'tideDashboardFilters/setPatientTagsFilter', payload: ['tag2'] },
{ type: 'tideDashboard/setOffset', payload: 0 },
]);
});
});
Loading