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

🚑 Fix Processed Trip Survey Responses Not Matching #1125

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
215 changes: 215 additions & 0 deletions www/__tests__/inputMatcher.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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: '<processed TripConfirmSurvey response>',
start_ts: 1000,
end_ts: 3000,
},
metadata: 'foo',
},
},
additions: [
{
data: {
name: 'TimeSurvey',
xmlResponse: '<processed TimeSurvey response>',
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: '<processed TimeSurvey response>',
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: '<processed TripConfirmSurvey response>' },
},
},
});
expect(notesMap['trip1'].length).toBe(1);
expect(notesMap['trip1'][0]).toMatchObject({
data: { xmlResponse: '<processed TimeSurvey response>' },
});
});
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: '<unprocessed TripConfirmSurvey response>',
start_ts: 5000,
end_ts: 7000,
});
await window['cordova'].plugins.BEMUserCache.putMessage('manual/trip_addition_input', {
name: 'TimeSurvey',
xmlResponse: '<unprocessed TimeSurvey response>',
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: '<processed TripConfirmSurvey response>' },
},
},
trip2: {
SURVEY: {
data: { xmlResponse: '<unprocessed TripConfirmSurvey response>' },
},
},
});

expect(notesMap['trip1'].length).toBe(1);
expect(notesMap['trip1'][0]).toMatchObject({
data: { xmlResponse: '<processed TimeSurvey response>' },
});

expect(notesMap['trip2'].length).toBe(2);
expect(notesMap['trip2'][0]).toMatchObject({
data: { xmlResponse: '<unprocessed TimeSurvey response>' },
});
expect(notesMap['trip2'][1]).toMatchObject({
data: { xmlResponse: '<processed TimeSurvey response>' },
});
});
});
11 changes: 8 additions & 3 deletions www/js/survey/inputMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions www/js/survey/multilabel/confirmHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,11 @@ export const getFakeEntry = (otherValue): Partial<LabelOption> | 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;
Expand Down
Loading