diff --git a/www/__tests__/inputMatcher.test.ts b/www/__tests__/inputMatcher.test.ts index 7e3b8fb81..5c0b248d9 100644 --- a/www/__tests__/inputMatcher.test.ts +++ b/www/__tests__/inputMatcher.test.ts @@ -1,3 +1,6 @@ +import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; +import { mockLogger } from '../__mocks__/globalMocks'; +import { unprocessedLabels, updateLocalUnprocessedInputs } from '../js/diary/timelineHelper'; import { EnketoUserInputEntry } from '../js/survey/enketo/enketoHelper'; import { fmtTs, @@ -8,9 +11,14 @@ import { getUserInputForTimelineEntry, getAdditionsForTimelineEntry, getUniqueEntries, + mapInputsToTimelineEntries, } from '../js/survey/inputMatcher'; +import { AppConfig } from '../js/types/appConfigTypes'; import { CompositeTrip, TimelineEntry, UserInputEntry } from '../js/types/diaryTypes'; +mockLogger(); +mockBEMUserCache(); + describe('input-matcher', () => { let userTrip: UserInputEntry; let trip: TimelineEntry; @@ -267,3 +275,210 @@ describe('input-matcher', () => { expect(uniqueEntires).toMatchObject([]); }); }); + +describe('mapInputsToTimelineEntries on a MULTILABEL configuration', () => { + const fakeConfigMultilabel = { + intro: {}, + survey_info: { + 'trip-labels': 'MULTILABEL', + }, + } as AppConfig; + + const timelineEntriesMultilabel = [ + { + _id: { $oid: 'trip1' }, + origin_key: 'analysis/confirmed_trip', + start_ts: 1000, + end_ts: 3000, + user_input: { + mode_confirm: 'walk', + }, + }, + { + _id: { $oid: 'placeA' }, + origin_key: 'analysis/confirmed_place', + enter_ts: 3000, + exit_ts: 5000, + // no user input + additions: [{ data: 'foo', metadata: 'bar' }], + }, + { + _id: { $oid: 'trip2' }, + origin_key: 'analysis/confirmed_trip', + start_ts: 5000, + end_ts: 7000, + // no user input + }, + ] as any as TimelineEntry[]; + it('creates a map that has the processed labels and notes', () => { + const [labelMap, notesMap] = mapInputsToTimelineEntries( + timelineEntriesMultilabel, + fakeConfigMultilabel, + ); + expect(labelMap).toMatchObject({ + trip1: { + MODE: { data: { label: 'walk' } }, + }, + }); + }); + it('creates a map that combines processed and unprocessed labels and notes', async () => { + // insert some unprocessed data + await window['cordova'].plugins.BEMUserCache.putMessage('manual/purpose_confirm', { + label: 'recreation', + start_ts: 1000, + end_ts: 3000, + }); + await window['cordova'].plugins.BEMUserCache.putMessage('manual/mode_confirm', { + label: 'bike', + start_ts: 5000, + end_ts: 7000, + }); + await updateLocalUnprocessedInputs({ start_ts: 1000, end_ts: 5000 }, fakeConfigMultilabel); + + // check that both processed and unprocessed data are returned + const [labelMap, notesMap] = mapInputsToTimelineEntries( + timelineEntriesMultilabel, + fakeConfigMultilabel, + ); + + expect(labelMap).toMatchObject({ + trip1: { + MODE: { data: { label: 'walk' } }, + PURPOSE: { data: { label: 'recreation' } }, + }, + trip2: { + MODE: { data: { label: 'bike' } }, + }, + }); + }); +}); + +describe('mapInputsToTimelineEntries on an ENKETO configuration', () => { + const fakeConfigEnketo = { + intro: {}, + survey_info: { + 'trip-labels': 'ENKETO', + buttons: { + 'trip-notes': { surveyName: 'TimeSurvey' }, + }, + surveys: { TripConfirmSurvey: { compatibleWith: 1 } }, + }, + } as any as AppConfig; + const timelineEntriesEnketo = [ + { + _id: { $oid: 'trip1' }, + origin_key: 'analysis/confirmed_trip', + start_ts: 1000, + end_ts: 3000, + user_input: { + trip_user_input: { + data: { + name: 'TripConfirmSurvey', + version: 1, + xmlResponse: '', + start_ts: 1000, + end_ts: 3000, + }, + metadata: 'foo', + }, + }, + additions: [ + { + data: { + name: 'TimeSurvey', + xmlResponse: '', + start_ts: 1000, + end_ts: 2000, + }, + metadata: 'foo', + }, + ], + }, + { + _id: { $oid: 'trip2' }, + origin_key: 'analysis/confirmed_trip', + start_ts: 5000, + end_ts: 7000, + // no user input + additions: [ + { + data: { + name: 'TimeSurvey', + xmlResponse: '', + match_id: 'foo', + start_ts: 5000, + end_ts: 7000, + }, + metadata: 'foo', + }, + ], + }, + ] as any as TimelineEntry[]; + it('creates a map that has the processed responses and notes', () => { + const [labelMap, notesMap] = mapInputsToTimelineEntries( + timelineEntriesEnketo, + fakeConfigEnketo, + ); + expect(labelMap).toMatchObject({ + trip1: { + SURVEY: { + data: { xmlResponse: '' }, + }, + }, + }); + expect(notesMap['trip1'].length).toBe(1); + expect(notesMap['trip1'][0]).toMatchObject({ + data: { xmlResponse: '' }, + }); + }); + it('creates a map that combines processed and unprocessed responses and notes', async () => { + // insert some unprocessed data + await window['cordova'].plugins.BEMUserCache.putMessage('manual/trip_user_input', { + name: 'TripConfirmSurvey', + version: 1, + xmlResponse: '', + start_ts: 5000, + end_ts: 7000, + }); + await window['cordova'].plugins.BEMUserCache.putMessage('manual/trip_addition_input', { + name: 'TimeSurvey', + xmlResponse: '', + match_id: 'bar', + start_ts: 6000, + end_ts: 7000, + }); + await updateLocalUnprocessedInputs({ start_ts: 1000, end_ts: 5000 }, fakeConfigEnketo); + + // check that both processed and unprocessed data are returned + const [labelMap, notesMap] = mapInputsToTimelineEntries( + timelineEntriesEnketo, + fakeConfigEnketo, + ); + + expect(labelMap).toMatchObject({ + trip1: { + SURVEY: { + data: { xmlResponse: '' }, + }, + }, + trip2: { + SURVEY: { + data: { xmlResponse: '' }, + }, + }, + }); + + expect(notesMap['trip1'].length).toBe(1); + expect(notesMap['trip1'][0]).toMatchObject({ + data: { xmlResponse: '' }, + }); + + expect(notesMap['trip2'].length).toBe(2); + expect(notesMap['trip2'][0]).toMatchObject({ + data: { xmlResponse: '' }, + }); + expect(notesMap['trip2'][1]).toMatchObject({ + data: { xmlResponse: '' }, + }); + }); +}); diff --git a/www/js/survey/inputMatcher.ts b/www/js/survey/inputMatcher.ts index bf6395ee1..a7e6b5c6e 100644 --- a/www/js/survey/inputMatcher.ts +++ b/www/js/survey/inputMatcher.ts @@ -2,7 +2,11 @@ import { logDebug, displayErrorMsg } from '../plugin/logger'; import { DateTime } from 'luxon'; import { CompositeTrip, ConfirmedPlace, TimelineEntry, UserInputEntry } from '../types/diaryTypes'; import { keysForLabelInputs, unprocessedLabels, unprocessedNotes } from '../diary/timelineHelper'; -import { getLabelInputDetails, inputType2retKey } from './multilabel/confirmHelper'; +import { + getLabelInputDetails, + inputType2retKey, + removeManualPrefix, +} from './multilabel/confirmHelper'; import { TimelineLabelMap, TimelineNotesMap } from '../diary/LabelTabContext'; import { MultilabelKey } from '../types/labelTypes'; import { EnketoUserInputEntry } from './enketo/enketoHelper'; @@ -281,7 +285,8 @@ export function mapInputsToTimelineEntries( timelineLabelMap[tlEntry._id.$oid] = { SURVEY: userInputForTrip }; } else { let processedSurveyResponse; - for (const key of keysForLabelInputs(appConfig)) { + for (const dataKey of keysForLabelInputs(appConfig)) { + const key = removeManualPrefix(dataKey); if (tlEntry.user_input?.[key]) { processedSurveyResponse = tlEntry.user_input[key]; break; @@ -293,7 +298,7 @@ export function mapInputsToTimelineEntries( // MULTILABEL configuration: use the label inputs from the labelOptions to determine which // keys to look for in the unprocessedInputs const labelsForTrip: { [k: string]: UserInputEntry | undefined } = {}; - Object.keys(getLabelInputDetails()).forEach((label: MultilabelKey) => { + Object.keys(getLabelInputDetails(appConfig)).forEach((label: MultilabelKey) => { // Check unprocessed labels first since they are more recent const userInputForTrip = getUserInputForTimelineEntry( tlEntry, diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index d21e34857..faba8e88b 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -137,8 +137,11 @@ export const getFakeEntry = (otherValue): Partial | undefined => { export const labelKeyToRichMode = (labelKey: string) => labelOptionByValue(labelKey, 'MODE')?.text || labelKeyToReadable(labelKey); -/* manual/mode_confirm becomes mode_confirm */ -export const inputType2retKey = (inputType) => getLabelInputDetails()[inputType].key.split('/')[1]; +/** @description e.g. manual/mode_confirm becomes mode_confirm */ +export const removeManualPrefix = (key: string) => key.split('/')[1]; +/** @description e.g. 'MODE' gets looked up, its key is 'manual/mode_confirm'. Returns without prefix as 'mode_confirm' */ +export const inputType2retKey = (inputType: string) => + removeManualPrefix(getLabelInputDetails()[inputType].key); export function verifiabilityForTrip(trip: CompositeTrip, userInputForTrip?: UserInputMap) { let allConfirmed = true;