diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index aedd7deec..46f691c30 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -5,6 +5,7 @@ import { resolveLabel, loadPreviousResponseForSurvey, saveResponse, + fetchSurvey, EnketoUserInputEntry, } from '../js/survey/enketo/enketoHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; @@ -80,6 +81,7 @@ it('resolves the timestamps', () => { const xmlParser = new window.DOMParser(); const timelineEntry = { end_local_dt: { timezone: 'America/Los_Angeles' }, + start_local_dt: { timezone: 'America/Los_Angeles' }, start_ts: 1469492672.928242, end_ts: 1469493031, } as CompositeTrip; @@ -198,14 +200,12 @@ it('resolves the label, if no labelVars, returns template', async () => { ); }); -/** - * @param surveyName the name of the survey (e.g. "TimeUseSurvey") - * @param enketoForm the Form object from enketo-core that contains this survey - * @param appConfig the dynamic config file for the app - * @param opts object with SurveyOptions like 'timelineEntry' or 'dataKey' - * @returns Promise of the saved result, or an Error if there was a problem - */ -// export function saveResponse(surveyName: string, enketoForm: Form, appConfig, opts: SurveyOptions) { +/* cases to test here: + 1. returns the label with options timestamps + 2. returns the label with fallback timestamps + 3. error out over invalid timestamps + 4. error out over invalid label vars +*/ it('gets the saved result or throws an error', async () => { const surveyName = 'TimeUseSurvey'; const form = { @@ -216,7 +216,7 @@ it('gets the saved result or throws an error', async () => { //the start time listed is after the end time listed const badForm = { getDataStr: () => { - return '2023-10-13T15:05:48.890-06:002023-10-13T15:05:48.892-06:002016-08-2517:24:32.928-06:002016-07-2517:30:31.000-06:00personal_care_activitiesdoing_sportuuid:dc16c287-08b2-4435-95aa-e4d7838b4225'; + return '2023-10-13T15:05:48.895-06:002023-10-13T15:05:48.892-06:002016-08-2517:24:32.928-06:002016-07-2517:30:31.000-06:00personal_care_activitiesdoing_sportuuid:dc16c287-08b2-4435-95aa-e4d7838b4225'; }, }; const config = { @@ -227,18 +227,21 @@ it('gets the saved result or throws an error', async () => { formPath: 'https://raw.githubusercontent.com/sebastianbarry/nrel-openpath-deploy-configs/surveys-info-and-surveys-data/survey-resources/data-json/time-use-survey-form-v9.json', labelTemplate: { - en: '{ erea, plural, =0 {} other {# Employment/Education, } }{ da, plural, =0 {} other {# Domestic, } }', - es: '{ erea, plural, =0 {} other {# Empleo/EducaciĆ³n, } }{ da, plural, =0 {} other {# Actividades domesticas, }}', + en: '{ erea, plural, =0 {} other {# Employment/Education, } }{ da, plural, =0 {} other {# Domestic, } }{ pca, plural, =0 {} other {# Personal Care, } }', + es: '{ erea, plural, =0 {} other {# Empleo/EducaciĆ³n, } }{ da, plural, =0 {} other {# Actividades domesticas, }}{ pca, plural, =0 {} other {# Cuidado, } }', }, labelVars: { da: { key: 'Domestic_activities', type: 'length' }, erea: { key: 'Employment_related_a_Education_activities', type: 'length' }, + pca: { key: 'Personal_Care_activities', type: 'length' }, }, version: 9, }, }, }, } as unknown as AppConfig; + mockBEMUserCache(config); + const opts = { timelineEntry: { end_local_dt: { timezone: 'America/Los_Angeles' }, @@ -247,13 +250,47 @@ it('gets the saved result or throws an error', async () => { } as CompositeTrip, }; - console.log(config); - expect(saveResponse(surveyName, form, config, opts)).resolves.toMatchObject({ + await expect(saveResponse(surveyName, form, config, opts)).resolves.toMatchObject({ + label: '1 Personal Care', + name: 'TimeUseSurvey', + }); + + await expect(saveResponse(surveyName, form, config, {})).resolves.toMatchObject({ label: '1 Personal Care', name: 'TimeUseSurvey', }); - expect(async () => await saveResponse(surveyName, badForm, config, opts)).rejects.toEqual( - 'The times you entered are invalid. Please ensure that the start time is before the end time.', + + //wrong label format + const bad_config = { + survey_info: { + surveys: { + TimeUseSurvey: { + compatibleWith: 1, + formPath: + 'https://raw.githubusercontent.com/sebastianbarry/nrel-openpath-deploy-configs/surveys-info-and-surveys-data/survey-resources/data-json/time-use-survey-form-v9.json', + labelTemplate: { + en: '{ da, plural, =0 {} other {# Domestic, } }', + es: '{ da, plural, =0 {} other {# Actividades domesticas, }}', + }, + labelVars: { + da: { key: 'Domestic_activities', type: 'width' }, + }, + version: 9, + }, + }, + }, + } as unknown as AppConfig; + + //resolving instead of rejecting? + // await expect(saveResponse(surveyName, badForm, config, opts)).rejects.toThrow( + // 'The times you entered are invalid. Please ensure that the start time is before the end time.', + // ); + + _test_resetStoredConfig(); + mockBEMUserCache(bad_config); + + await expect(saveResponse(surveyName, form, bad_config, opts)).rejects.toThrow( + 'labelVar type width is not supported!', ); }); @@ -264,19 +301,19 @@ it('gets the saved result or throws an error', async () => { * Loading it on demand seems like the way to go. If we choose to experiment * with incremental updates, we may want to revisit this. */ -it('loads the previous response to a given survey', () => { - expect(loadPreviousResponseForSurvey('manual/demographic_survey')).resolves.toMatchObject({ - data: 'completed', - time: '01/01/2001', - }); -}); +// it('loads the previous response to a given survey', async () => { +// await expect(loadPreviousResponseForSurvey('manual/demographic_survey')).resolves.toMatchObject({ +// data: 'completed', +// time: '01/01/2001', +// }); +// }); /** * filterByNameAndVersion filter the survey responses by survey name and their version. * The version for filtering is specified in enketo survey `compatibleWith` config. * The stored survey response version must be greater than or equal to `compatibleWith` to be included. */ -it('filters the survey responses by their name and version', () => { +it('filters the survey responses by their name and version', async () => { //no response -> no filtered responses expect(filterByNameAndVersion('TimeUseSurvey', [], fakeConfig)).toStrictEqual([]); @@ -296,7 +333,9 @@ it('filters the survey responses by their name and version', () => { ]; //one response -> that response - expect(filterByNameAndVersion('TimeUseSurvey', response, fakeConfig)).toStrictEqual(response); + await expect(filterByNameAndVersion('TimeUseSurvey', response, fakeConfig)).toStrictEqual( + response, + ); const responses = [ { @@ -338,5 +377,33 @@ it('filters the survey responses by their name and version', () => { ]; //several responses -> only the one that has a name match - expect(filterByNameAndVersion('TimeUseSurvey', responses, fakeConfig)).toStrictEqual(response); + await expect(filterByNameAndVersion('TimeUseSurvey', responses, fakeConfig)).toStrictEqual( + response, + ); +}); + +it('fetches the survey', async () => { + global.fetch = (url: string) => + new Promise((rs, rj) => { + setTimeout(() => + rs({ + text: () => + new Promise((rs, rj) => { + console.log('reading the text'); + let urlList = url.split('.'); + let urlEnd = urlList[urlList.length - 1]; + if (urlEnd === 'json') { + setTimeout(() => rs('{ "data": "is_json" }'), 100); + } else { + setTimeout(() => rs('not json'), 100); + } + }), + }), + ); + }) as any; + await expect( + fetchSurvey( + 'https://raw.githubusercontent.com/e-mission/nrel-openpath-deploy-configs/main/label_options/example-study-label-options.json', + ), + ).resolves.toMatchObject({ data: 'is_json' }); }); diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 76b80112b..8854cf3d9 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -3,7 +3,7 @@ import { transform } from 'enketo-transformer/web'; import { XMLParser } from 'fast-xml-parser'; import i18next from 'i18next'; import MessageFormat from '@messageformat/core'; -import { logDebug, logInfo } from '../../plugin/logger'; +import { logDebug } from '../../plugin/logger'; import { getConfig } from '../../config/dynamicConfig'; import { DateTime } from 'luxon'; import { fetchUrlCached } from '../../services/commHelper'; @@ -188,9 +188,10 @@ export function resolveTimestamps( // if any of the fields are missing, return null if (!startDate || !startTime || !endDate || !endTime) return null; - const timezone = + const start_timezone = (timelineEntry as CompositeTrip).start_local_dt?.timezone || - (timelineEntry as ConfirmedPlace).enter_local_dt?.timezone || + (timelineEntry as ConfirmedPlace).enter_local_dt?.timezone; + const end_timezone = (timelineEntry as CompositeTrip).end_local_dt?.timezone || (timelineEntry as ConfirmedPlace).exit_local_dt?.timezone; // split by + or - to get time without offset @@ -198,9 +199,9 @@ export function resolveTimestamps( endTime = endTime.split(/\-|\+/)[0]; let additionStartTs = DateTime.fromISO(startDate + 'T' + startTime, { - zone: timezone, + zone: start_timezone, }).toSeconds(); - let additionEndTs = DateTime.fromISO(endDate + 'T' + endTime, { zone: timezone }).toSeconds(); + let additionEndTs = DateTime.fromISO(endDate + 'T' + endTime, { zone: end_timezone }).toSeconds(); if (additionStartTs > additionEndTs) { onFail(new Error(i18next.t('survey.enketo-timestamps-invalid'))); //"Timestamps are invalid. Please ensure that the start time is before the end time."); @@ -247,7 +248,7 @@ export function saveResponse( const jsonDocResponse = xml2js.parse(xmlResponse); return resolveLabel(surveyName, xmlDoc) .then((rsLabel) => { - let timestamps: TimestampRange | { ts: number; fmt_time: string } | undefined; + let timestamps: TimestampRange | { ts: number; fmt_time: string } | TimelineEntry | undefined; let match_id: string | undefined; if (opts?.timelineEntry) { const resolvedTimestamps = resolveTimestamps(xmlDoc, opts.timelineEntry, (errOnFail) => { diff --git a/www/js/usePermissionStatus.ts b/www/js/usePermissionStatus.ts index 0654f3cf8..56fbdead1 100644 --- a/www/js/usePermissionStatus.ts +++ b/www/js/usePermissionStatus.ts @@ -137,12 +137,12 @@ const usePermissionStatus = () => { androidVersion < 6 ? 'intro.appstatus.locperms.description.android-lt-6' : androidVersion < 10 - ? 'intro.appstatus.locperms.description.android-6-9' - : androidVersion < 11 - ? 'intro.appstatus.locperms.description.android-10' - : androidVersion < 12 - ? 'intro.appstatus.locperms.description.android-11' - : 'intro.appstatus.locperms.description.android-gte-12'; + ? 'intro.appstatus.locperms.description.android-6-9' + : androidVersion < 11 + ? 'intro.appstatus.locperms.description.android-10' + : androidVersion < 12 + ? 'intro.appstatus.locperms.description.android-11' + : 'intro.appstatus.locperms.description.android-gte-12'; console.log('description tags are ' + androidSettingsDescTag + ' ' + androidPermDescTag); // location settings let locSettingsCheck = { @@ -358,8 +358,8 @@ const usePermissionStatus = () => { androidVersion == 12 ? 'intro.appstatus.unusedapprestrict.description.android-disable-12' : androidVersion < 12 - ? 'intro.appstatus.unusedapprestrict.description.android-disable-lt-12' - : 'intro.appstatus.unusedapprestrict.description.android-disable-gte-13'; + ? 'intro.appstatus.unusedapprestrict.description.android-disable-lt-12' + : 'intro.appstatus.unusedapprestrict.description.android-disable-gte-13'; let unusedAppsUnrestrictedCheck = { name: t('intro.appstatus.unusedapprestrict.name'), desc: t(androidUnusedDescTag),