Skip to content

Commit

Permalink
Merge pull request #1138 from JGreenlee/refactoring_timelinecontext
Browse files Browse the repository at this point in the history
⏱️ Implement TimelineContext (refactoring of LabelTabContext)
  • Loading branch information
shankari authored May 21, 2024
2 parents 1f5ae90 + b690c0c commit a16bbc0
Show file tree
Hide file tree
Showing 40 changed files with 1,110 additions and 747 deletions.
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', () => {
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', () => {
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 usePermissionStatus from './usePermissionStatus';
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 @@ const App = () => {
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 />;
} else {
// if there is an onboarding route that is not DONE, show the onboarding stack
appContent = <OnboardingStack />;
Expand Down
Loading

0 comments on commit a16bbc0

Please sign in to comment.