Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⏱️ Implement TimelineContext (refactoring of LabelTabContext) #1138

Merged
merged 38 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b1adc4f
Merge branch 'code-cleanup-jan2024' of https://github.com/JGreenlee/e…
JGreenlee Mar 14, 2024
b18e610
extract main content of <App> into new component, <Main>
JGreenlee Mar 14, 2024
0e86c12
refactor LabelTabContext into TimelineContext
JGreenlee Mar 19, 2024
8d34461
refactor TimelineContext to use dateRange
JGreenlee Mar 22, 2024
2546629
unify DateSelect for Label tab and Dashboard tab
JGreenlee Mar 22, 2024
49c4989
TimelineContext: update log statements & remove unused
JGreenlee Mar 22, 2024
45e3811
handle edge cases in loadSpecificWeek
JGreenlee Mar 22, 2024
1c195bf
DateSelect: use today as maximum, not pipeline end
JGreenlee Mar 22, 2024
368f6c8
DateSelect: close datepicker after chosen date(s)
JGreenlee Mar 22, 2024
1a12b8b
fix wrong dates date selections
JGreenlee Mar 22, 2024
f165a3f
compute user metrics on phone using e-mission-common
JGreenlee Mar 25, 2024
751c4d0
TimelineContext: refactor loadAnotherWeek -> loadMoreDays
JGreenlee Mar 29, 2024
94e4020
allow missing days for segmentDaysByWeeks
JGreenlee Apr 1, 2024
072c220
MetricsTab: load more data if timeline dateRange < 14 days
JGreenlee Apr 1, 2024
c5f6970
fix ts on DailyActiveMinutesCard
JGreenlee Apr 1, 2024
6c07e44
MetricsTab: add console.time to userMetrics computation
JGreenlee Apr 1, 2024
4a22227
cleanup code in MetricsTab
JGreenlee Apr 1, 2024
ed768a7
fix refreshTimeline()
JGreenlee Apr 1, 2024
126e46a
getFootprintForMetrics: use forEach instead of for..in
JGreenlee Apr 1, 2024
baf79c0
update e-mission-common import
JGreenlee Apr 1, 2024
13ce501
update e-mission-common import
JGreenlee Apr 1, 2024
be21343
Merge branches 'refactoring_timelinecontext' and 'refactoring_timelin…
JGreenlee Apr 1, 2024
0136eeb
add a bunch of tests for metricsHelper
JGreenlee Apr 1, 2024
cc7434f
Merge branch 'master' into refactoring_timelinecontext
JGreenlee Apr 1, 2024
dfd3376
fix ActiveMinutes components
JGreenlee Apr 2, 2024
4ca6e3f
add tests for TimelineContext
JGreenlee Apr 2, 2024
5ae3ea8
fix segmentDaysByWeeks
JGreenlee Apr 2, 2024
00c655c
only query draft trips in the dateRange
JGreenlee Apr 2, 2024
82e0382
bump e-mission-common to 0.1.3
JGreenlee Apr 2, 2024
fe4e6eb
TimelineContext: fix excessive draft trips
JGreenlee Apr 2, 2024
4da2761
add pulsing loading line at the bottom of NavBar
JGreenlee Apr 2, 2024
da7283e
don't show ChangeIndicator when change is undefined
JGreenlee Apr 3, 2024
1269aad
MetricsTab: const N_DAYS_TO_LOAD = 14
JGreenlee Apr 5, 2024
f1961a6
segmentDaysByWeeks: use for loop, not forEach
JGreenlee Apr 5, 2024
cf9aaf7
update e-mission-common to v0.3.2
JGreenlee Apr 12, 2024
21b9faa
Merge branch 'master' of https://github.com/e-mission/e-mission-phone…
JGreenlee Apr 17, 2024
91af26c
don't show infinite progressbar at top of Profile tab
JGreenlee Apr 17, 2024
b690c0c
Merge branch 'master' of https://github.com/e-mission/e-mission-phone…
JGreenlee May 13, 2024
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
4 changes: 3 additions & 1 deletion www/__mocks__/cordovaMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export const mockBEMUserCache = (config?) => {
}, 100),
);
} else {
return undefined;
return Promise.resolve([]);
}
},
};
Expand Down Expand Up @@ -229,6 +229,8 @@ export const mockBEMServerCom = () => {
}, 100);
},
};
window['cordova'] ||= {};
window['cordova'].plugins ||= {};
window['cordova'].plugins.BEMServerComm = mockBEMServerCom;
};

Expand Down
74 changes: 74 additions & 0 deletions www/__tests__/TimelineContext.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { useEffect } from 'react';
import { View, Text } from 'react-native';
import { act, render, screen, waitFor } from '@testing-library/react-native';
import { useTimelineContext } from '../js/TimelineContext';
import { mockLogger } from '../__mocks__/globalMocks';
import { mockBEMServerCom, mockBEMUserCache } from '../__mocks__/cordovaMocks';

mockLogger();
mockBEMUserCache();

jest.mock('../js/services/commHelper', () => ({
getPipelineRangeTs: jest.fn(() => Promise.resolve({ start_ts: 1, end_ts: 10 })),
getRawEntries: jest.fn((key_list, _, __) => {
let phone_data: any[] = [];
if (key_list.includes('analysis/composite_trip')) {
phone_data = [
{
_id: { $oid: 'trip1' },
metadata: { write_ts: 1, origin_key: 'analysis/confirmed_trip' },
data: { start_ts: 1, end_ts: 2 },
},
{
_id: { $oid: 'trip2' },
metadata: { write_ts: 2, origin_key: 'analysis/confirmed_trip' },
data: { start_ts: 3, end_ts: 4 },
},
{
_id: { $oid: 'trip3' },
metadata: { write_ts: 3, origin_key: 'analysis/confirmed_trip' },
data: { start_ts: 5, end_ts: 6 },
},
];
}
return Promise.resolve({ phone_data });
}),
fetchUrlCached: jest.fn(() => Promise.resolve(null)),
}));

// Mock useAppConfig default export
jest.mock('../js/useAppConfig', () => {
return jest.fn(() => ({ intro: {} }));
});

const TimelineContextTestComponent = () => {
const { timelineMap, setDateRange } = useTimelineContext();

useEffect(() => {
// setDateRange(['2021-01-01', '2021-01-07']);
}, []);

if (!timelineMap) return null;

console.debug('timelineMap', timelineMap);

return (
<View testID="timeline-entries">
{[...timelineMap.values()].map((entry, i) => (
<Text key={i}>{'entry ID: ' + entry._id.$oid}</Text>
))}
</View>
);
};

describe('TimelineContext', () => {
it('renders correctly', async () => {
render(<TimelineContextTestComponent />);
await waitFor(() => {
// make sure timeline entries are rendered
expect(screen.getByTestId('timeline-entries')).toBeTruthy();
// make sure number of Text components matches number of timeline entries
expect(screen.getAllByText(/entry ID:/).length).toBe(3);
});
});
});
2 changes: 1 addition & 1 deletion www/__tests__/confirmHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {

import initializedI18next from '../js/i18nextInit';
import { CompositeTrip, UserInputEntry } from '../js/types/diaryTypes';
import { UserInputMap } from '../js/diary/LabelTabContext';
import { UserInputMap } from '../js/TimelineContext';
window['i18next'] = initializedI18next;
mockLogger();

Expand Down
128 changes: 128 additions & 0 deletions www/__tests__/metricsHelper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
calculatePercentChange,
formatDate,
formatDateRangeOfDays,
getLabelsForDay,
getUniqueLabelsForDays,
segmentDaysByWeeks,
} from '../js/metrics/metricsHelper';
import {
DayOfClientMetricData,
DayOfMetricData,
DayOfServerMetricData,
} from '../js/metrics/metricsTypes';

describe('metricsHelper', () => {
describe('getUniqueLabelsForDays', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not really testing the uniqueness of the labels for the day. Since the labels are all unique, this seems identical to getLabelsForDay

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right; this should test a case where one label is present across multiple days
TODO

const days1 = [
{ label_a: 1, label_b: 2 },
{ label_c: 1, label_d: 3 },
] as any as DayOfServerMetricData[];
it("should return unique labels for days with 'label_*'", () => {
expect(getUniqueLabelsForDays(days1)).toEqual(['a', 'b', 'c', 'd']);
});

const days2 = [
{ mode_a: 1, mode_b: 2 },
{ mode_c: 1, mode_d: 3 },
] as any as DayOfClientMetricData[];
it("should return unique labels for days with 'mode_*'", () => {
expect(getUniqueLabelsForDays(days2)).toEqual(['a', 'b', 'c', 'd']);
});
});

describe('getLabelsForDay', () => {
const day1 = { label_a: 1, label_b: 2 } as any as DayOfServerMetricData;
it("should return labels for a day with 'label_*'", () => {
expect(getLabelsForDay(day1)).toEqual(['a', 'b']);
});

const day2 = { mode_a: 1, mode_b: 2 } as any as DayOfClientMetricData;
it("should return labels for a day with 'mode_*'", () => {
expect(getLabelsForDay(day2)).toEqual(['a', 'b']);
});
});

// secondsToMinutes

// secondsToHours

describe('segmentDaysByWeeks', () => {
const days1 = [
{ date: '2021-01-01' },
{ date: '2021-01-02' },
{ date: '2021-01-04' },
{ date: '2021-01-08' },
{ date: '2021-01-09' },
{ date: '2021-01-10' },
] as any as DayOfClientMetricData[];

it("should segment days with 'date' into weeks", () => {
expect(segmentDaysByWeeks(days1, '2021-01-10')).toEqual([
// most recent week
[
{ date: '2021-01-04' },
{ date: '2021-01-08' },
{ date: '2021-01-09' },
{ date: '2021-01-10' },
],
// prior week
[{ date: '2021-01-01' }, { date: '2021-01-02' }],
]);
});

const days2 = [
{ fmt_time: '2021-01-01T00:00:00Z' },
{ fmt_time: '2021-01-02T00:00:00Z' },
{ fmt_time: '2021-01-04T00:00:00Z' },
{ fmt_time: '2021-01-08T00:00:00Z' },
{ fmt_time: '2021-01-09T00:00:00Z' },
{ fmt_time: '2021-01-10T00:00:00Z' },
] as any as DayOfServerMetricData[];
it("should segment days with 'fmt_time' into weeks", () => {
expect(segmentDaysByWeeks(days2, '2021-01-10')).toEqual([
// most recent week
[
{ fmt_time: '2021-01-04T00:00:00Z' },
{ fmt_time: '2021-01-08T00:00:00Z' },
{ fmt_time: '2021-01-09T00:00:00Z' },
{ fmt_time: '2021-01-10T00:00:00Z' },
],
// prior week
[{ fmt_time: '2021-01-01T00:00:00Z' }, { fmt_time: '2021-01-02T00:00:00Z' }],
]);
});
});

describe('formatDate', () => {
const day1 = { date: '2021-01-01' } as any as DayOfClientMetricData;
it('should format date', () => {
expect(formatDate(day1)).toEqual('1/1');
});

const day2 = { fmt_time: '2021-01-01T00:00:00Z' } as any as DayOfServerMetricData;
it('should format date', () => {
expect(formatDate(day2)).toEqual('1/1');
});
});

describe('formatDateRangeOfDays', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should also add tests for the part before or after the - being blank.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean by this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean that you should have tests that result in strings of the form 1/1 - . Isn't that how we represent a range that is open at the end? Although maybe after the refactoring to always use ranges, we always have a start and an end?

const days1 = [
{ date: '2021-01-01' },
{ date: '2021-01-02' },
{ date: '2021-01-04' },
] as any as DayOfClientMetricData[];
it('should format date range for days with date', () => {
expect(formatDateRangeOfDays(days1)).toEqual('1/1 - 1/4');
});

const days2 = [
{ fmt_time: '2021-01-01T00:00:00Z' },
{ fmt_time: '2021-01-02T00:00:00Z' },
{ fmt_time: '2021-01-04T00:00:00Z' },
] as any as DayOfServerMetricData[];
it('should format date range for days with fmt_time', () => {
expect(formatDateRangeOfDays(days2)).toEqual('1/1 - 1/4');
});
});
});
65 changes: 4 additions & 61 deletions www/js/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import React, { useEffect, useState, createContext, useMemo } from 'react';
import { ActivityIndicator, BottomNavigation, useTheme } from 'react-native-paper';
import { useTranslation } from 'react-i18next';
import LabelTab from './diary/LabelTab';
import MetricsTab from './metrics/MetricsTab';
import ProfileSettings from './control/ProfileSettings';
import React, { useEffect, useState, createContext } from 'react';
import { ActivityIndicator } from 'react-native-paper';
import useAppConfig from './useAppConfig';
import OnboardingStack from './onboarding/OnboardingStack';
import {
Expand All @@ -17,58 +13,18 @@
import { initPushNotify } from './splash/pushNotifySettings';
import { initStoreDeviceSettings } from './splash/storeDeviceSettings';
import { initRemoteNotifyHandler } from './splash/remoteNotifyHandler';
import { withErrorBoundary } from './plugin/ErrorBoundary';
import { initCustomDatasetHelper } from './metrics/customMetricsHelper';
import AlertBar from './components/AlertBar';

const defaultRoutes = (t) => [
{
key: 'label',
title: t('diary.label-tab'),
focusedIcon: 'check-bold',
unfocusedIcon: 'check-outline',
accessibilityLabel: t('diary.label-tab'),
},
{
key: 'metrics',
title: t('metrics.dashboard-tab'),
focusedIcon: 'chart-box',
unfocusedIcon: 'chart-box-outline',
accessibilityLabel: t('metrics.dashboard-tab'),
},
{
key: 'control',
title: t('control.profile-tab'),
focusedIcon: 'account',
unfocusedIcon: 'account-outline',
accessibilityLabel: t('control.profile-tab'),
},
];
import Main from './Main';

export const AppContext = createContext<any>({});

const scenes = {
label: withErrorBoundary(LabelTab),
metrics: withErrorBoundary(MetricsTab),
control: withErrorBoundary(ProfileSettings),
};

const App = () => {
const [index, setIndex] = useState(0);
// will remain null while the onboarding state is still being determined
const [onboardingState, setOnboardingState] = useState<OnboardingState | null>(null);
const [permissionsPopupVis, setPermissionsPopupVis] = useState(false);
const appConfig = useAppConfig();
const permissionStatus = usePermissionStatus();
const { colors } = useTheme();
const { t } = useTranslation();

const routes = useMemo(() => {
const showMetrics = appConfig?.survey_info?.['trip-labels'] == 'MULTILABEL';
return showMetrics ? defaultRoutes(t) : defaultRoutes(t).filter((r) => r.key != 'metrics');
}, [appConfig, t]);

const renderScene = BottomNavigation.SceneMap(scenes);

const refreshOnboardingState = () => getPendingOnboardingState().then(setOnboardingState);
useEffect(() => {
Expand Down Expand Up @@ -102,20 +58,7 @@
appContent = <ActivityIndicator size={'large'} style={{ flex: 1 }} />;
} else if (onboardingState?.route == OnboardingRoute.DONE) {
// if onboarding route is DONE, show the main app with navigation between tabs
appContent = (
<BottomNavigation
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
// Place at bottom, color of 'surface' (white) by default, and 68px tall (default was 80)
safeAreaInsets={{ bottom: 0 }}
style={{ backgroundColor: colors.surface }}
barStyle={{ height: 68, justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0)' }}
// BottomNavigation uses secondaryContainer color for the background, but we want primaryContainer
// (light blue), so we override here.
theme={{ colors: { secondaryContainer: colors.primaryContainer } }}
/>
);
appContent = <Main />;

Check warning on line 61 in www/js/App.tsx

View check run for this annotation

Codecov / codecov/patch

www/js/App.tsx#L61

Added line #L61 was not covered by tests
} else {
// if there is an onboarding route that is not DONE, show the onboarding stack
appContent = <OnboardingStack />;
Expand Down
Loading
Loading