From 1830680b3182c4e0f22a27b946c38629627dd982 Mon Sep 17 00:00:00 2001 From: Rahul Keerthi Date: Tue, 9 Jan 2024 10:44:35 +0000 Subject: [PATCH] feat(slider): implement new slider from ui-lib, add tests for slider-based forms (#196) --- package.json | 4 +- pages/index.tsx | 5 + public/locales/en/common.json | 3 + spec/mocks/index.ts | 1 + spec/mocks/testFormFixture.ts | 518 ++++++++++++++++++ src/components/Form/Form.spec.tsx | 187 +++++++ src/components/Form/Form.tsx | 3 + .../useHostedSession/useHostedSession.ts | 10 +- yarn.lock | 8 +- 9 files changed, 729 insertions(+), 10 deletions(-) create mode 100644 spec/mocks/testFormFixture.ts create mode 100644 src/components/Form/Form.spec.tsx diff --git a/package.json b/package.json index aaab25b3..ffb06891 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hosted-pages", - "version": "0.0.79", + "version": "0.0.80", "private": true, "scripts": { "dev": "next dev", @@ -14,7 +14,7 @@ }, "dependencies": { "@apollo/client": "^3.8.6", - "@awell-health/ui-library": "0.1.35", + "@awell-health/ui-library": "0.1.37", "@formsort/react-embed": "^3.1.1", "@sentry/nextjs": "^7.59.3", "date-fns": "^2.29.3", diff --git a/pages/index.tsx b/pages/index.tsx index 3ad82d14..97926098 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -144,6 +144,11 @@ const Home: NextPageWithLayout = () => { {/* Show static success page if success URL is not available */} {shouldRedirect === false && diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 5aceec73..397181a5 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -38,6 +38,9 @@ "select": { "search_placeholder": "Search for an option", "no_options": "No options found" + }, + "slider": { + "tooltip_guide": "Touch to indicate your answer" } } }, diff --git a/spec/mocks/index.ts b/spec/mocks/index.ts index 8b9870ba..5433e379 100644 --- a/spec/mocks/index.ts +++ b/spec/mocks/index.ts @@ -1 +1,2 @@ export * from './activities' +export * from './testFormFixture' diff --git a/spec/mocks/testFormFixture.ts b/spec/mocks/testFormFixture.ts new file mode 100644 index 00000000..b8a8ebba --- /dev/null +++ b/spec/mocks/testFormFixture.ts @@ -0,0 +1,518 @@ +import { + DataPointValueType, + Form, + QuestionType, + UserQuestionType, +} from '@awell-health/ui-library' + +export const form: Form = { + id: 'Kzr2NafTxJfR', + title: 'Example form', + key: 'exampleForm', + definition_id: '', + release_id: '', + questions: [ + { + id: 'x5bgJqsOltmK3', + title: 'Single select question', + definition_id: 'Kzr2NafTxJfR', + key: 'singleSelectQuestion', + dataPointValueType: DataPointValueType.Number, + options: [ + { + id: 'q01_1', + label: 'Option 1', + value: 0, + }, + { + id: 'q01_2', + label: 'Option 2', + value: 1, + }, + { + id: 'q01_3', + label: 'Option 3', + value: 3, + }, + { + id: 'q01_4', + label: 'Option 4', + value: 4, + }, + { + id: 'q01_5', + label: 'Option 5', + value: 5, + }, + ], + questionType: QuestionType.MultipleChoice, + userQuestionType: UserQuestionType.MultipleChoice, + questionConfig: { + recode_enabled: false, + mandatory: false, + slider: null, + }, + }, + { + id: 'x5bgJqOltmK3', + title: 'Single select question', + definition_id: 'Kzr2NafTxJfR', + key: 'singleSelectQuestion', + dataPointValueType: DataPointValueType.Number, + options: [ + { + id: 'q02_1', + label: 'Option 1', + value: 0, + }, + { + id: 'q02_2', + label: 'Option 2', + value: 1, + }, + { + id: 'q02_3', + label: 'Option 3', + value: 3, + }, + { + id: 'q02_4', + label: 'Option 4', + value: 4, + }, + { + id: 'q02_5', + label: 'Option 5', + value: 5, + }, + ], + questionType: QuestionType.MultipleChoice, + userQuestionType: UserQuestionType.MultipleChoice, + questionConfig: { + recode_enabled: false, + mandatory: true, + slider: null, + }, + }, + { + id: 'HyIaUkgDcXwR', + title: 'This is multiple select question', + definition_id: 'Kzr2NafTxJfR', + key: 'thisIsMultipleSelectQuestion', + dataPointValueType: DataPointValueType.NumbersArray, + options: [ + { + id: 'q3_ZT6yN64opulL', + value: 0, + label: 'Option 1', + }, + { + id: 'q3_RqYicEXp0agy', + value: 1, + label: 'Option 2', + }, + { + id: 'q3_d_uA9ldC6L8_', + value: 2, + label: 'Option 3', + }, + { + id: 'q3_ZkhlOC3DfSOF', + value: 3, + label: 'Option 4', + }, + ], + questionType: QuestionType.MultipleChoice, + userQuestionType: UserQuestionType.MultipleSelect, + questionConfig: { + recode_enabled: false, + mandatory: false, + slider: null, + }, + }, + { + id: 'VkL1vrscT5sdMV', + title: 'This is date question', + definition_id: 'Kzar2NafTxJfR', + key: 'thisIsDateQuestion', + dataPointValueType: DataPointValueType.Date, + options: [], + questionType: QuestionType.Input, + userQuestionType: UserQuestionType.Date, + questionConfig: { + recode_enabled: false, + mandatory: false, + slider: null, + }, + }, + { + id: 'VkL1vrscT5MV', + title: 'This is yes or no question', + definition_id: 'Kzr2NafTxJfR', + key: 'thisIsYesOrNoQuestion', + dataPointValueType: DataPointValueType.Boolean, + options: [], + questionType: QuestionType.Input, + userQuestionType: UserQuestionType.YesNo, + questionConfig: { + recode_enabled: false, + mandatory: true, + slider: null, + }, + }, + { + id: 'fSN5BktQ6cOV', + title: + '[{"type":"p","children":[{"text":"This ","bold":true},{"text":"is","italic":true},{"text":" "},{"text":"rich text","strikethrough":true},{"text":" "},{"text":"description","underline":true}]}]', + definition_id: 'Kzr2NafTxJfR', + key: 'typePChildrenTextThisBoldTrueTextIsItalicTrueTextTextRichTextStrikethroughTrueTextTextDescriptionUnderlineTrue', + dataPointValueType: null, + options: [], + questionType: QuestionType.NoInput, + userQuestionType: UserQuestionType.Description, + questionConfig: { + recode_enabled: false, + mandatory: false, + slider: null, + }, + }, + { + id: 'XAgYxu_kbDPj', + title: 'This is slider question', + definition_id: 'Kzr2NafTxJfR', + key: 'thisIsSliderQuestion', + dataPointValueType: DataPointValueType.Number, + options: [], + questionType: QuestionType.Input, + userQuestionType: UserQuestionType.Slider, + questionConfig: { + recode_enabled: false, + mandatory: true, + slider: { + min: 0, + max: 10, + step_value: 1, + display_marks: true, + min_label: 'Min', + max_label: 'Max', + is_value_tooltip_on: true, + show_min_max_values: true, + }, + }, + }, + { + id: '5KMcDYtoz0rr', + title: 'This is number question', + definition_id: 'Kzr2NafTxJfR', + key: 'thisIsNumberQuestion', + dataPointValueType: DataPointValueType.Number, + options: [], + questionType: QuestionType.Input, + userQuestionType: UserQuestionType.Number, + questionConfig: { + recode_enabled: false, + mandatory: false, + slider: null, + }, + }, + { + id: 'U99uUQ_Jp5Jb', + title: 'This is short text question', + definition_id: 'Kzr2NafTxJfR', + key: 'thisIsShortTextQuestion', + dataPointValueType: DataPointValueType.String, + options: [], + questionType: QuestionType.Input, + userQuestionType: UserQuestionType.ShortText, + questionConfig: { + recode_enabled: false, + mandatory: false, + slider: null, + }, + }, + { + id: '6mv3n9HaXFTU', + title: 'This is long text question', + definition_id: 'Kzr2NafTxJfR', + key: 'thisIsLongTextQuestion', + dataPointValueType: DataPointValueType.String, + options: [], + questionType: QuestionType.Input, + userQuestionType: UserQuestionType.LongText, + questionConfig: { + recode_enabled: false, + mandatory: false, + slider: null, + }, + }, + { + id: 'HyIaUksdgDcXwR', + title: 'This is a phone number question', + definition_id: 'Kzr2NafsaaTxJfR', + key: 'thisIsPhoneNumberQuestion', + dataPointValueType: DataPointValueType.Telephone, + options: [ + { + id: 'q9_ZT6yN64opulL', + value: 0, + label: 'Option 1', + }, + { + id: 'q9_RqYicEXp0agy', + value: 1, + label: 'Option 2', + }, + { + id: 'q9_d_uA9ldC6L8_', + value: 2, + label: 'Option 3', + }, + { + id: 'q9_ZkhlOC3DfSOF', + value: 3, + label: 'Option 4', + }, + ], + questionType: QuestionType.Input, + userQuestionType: UserQuestionType.Telephone, + questionConfig: { + recode_enabled: false, + mandatory: false, + slider: null, + }, + }, + { + id: 'HyIaUasadkgDcXwR', + title: 'This is multiple select question - with Select input', + definition_id: 'Kzr2NafsaaTxJfR', + key: 'thisIsMultipleSelectQuestionWithSelectInput', + dataPointValueType: DataPointValueType.NumbersArray, + options: [ + { + id: 'q10_ZT6yN64opulL', + value: 0, + label: 'Option 1', + }, + { + id: 'q10_RqYicEXp0agy', + value: 1, + label: 'Option 2', + }, + { + id: 'q10_d_uA9ldC6L8_', + value: 2, + label: 'Option 3', + }, + { + id: 'q10_ZkhlOC3DfSOF', + value: 3, + label: 'Option 4', + }, + ], + questionType: QuestionType.MultipleChoice, + userQuestionType: UserQuestionType.MultipleSelect, + questionConfig: { + recode_enabled: false, + mandatory: false, + slider: null, + use_select: true, + }, + }, + { + id: 'HyIaUkgDczxXwR', + title: 'This is multiple choice question - with Select input', + definition_id: 'Kzr2NasfTxJfR', + key: 'thisIsMultipleChoiceQuestionWithSelectInput', + dataPointValueType: DataPointValueType.Number, + options: [ + { + id: 'q11_ZT6yN64opulL', + value: 0, + label: 'Option 1', + }, + { + id: 'q11_RqYicEXp0agy', + value: 1, + label: 'Option 2', + }, + { + id: 'q11_d_uA9ldC6L8_', + value: 2, + label: 'Option 3', + }, + { + id: 'q11_ZkhlOC3DfSOF', + value: 3, + label: 'Option 4', + }, + ], + questionType: QuestionType.MultipleChoice, + userQuestionType: UserQuestionType.MultipleChoice, + questionConfig: { + recode_enabled: false, + mandatory: false, + slider: null, + use_select: true, + }, + }, + ], +} + +export const formWithTwoRequiredSingleSelectQuestions: Form = { + id: 'Kzr2NafTxJfR', + title: 'Example form', + key: 'exampleForm', + definition_id: '', + release_id: '', + questions: [ + { + id: 'x5bgJqOltmK3', + title: 'Single select question #1', + definition_id: 'Kzr2NafTxJfR', + key: 'singleSelectQuestion', + dataPointValueType: DataPointValueType.Number, + options: [ + { + id: 'q1_1', + label: 'Answer the first required question', + value: 0, + }, + { + id: 'q1_2', + label: 'Option 2', + value: 1, + }, + ], + questionType: QuestionType.MultipleChoice, + userQuestionType: UserQuestionType.MultipleChoice, + questionConfig: { + recode_enabled: false, + mandatory: true, + slider: null, + }, + }, + { + id: 'x5bgJqOltmK3', + title: 'Single select question #2', + definition_id: 'Kzr2NafTxJfR', + key: 'singleSelectQuestion', + dataPointValueType: DataPointValueType.Number, + options: [ + { + id: 'q2_1', + label: 'Option 1', + value: 0, + }, + { + id: 'q2_2', + label: 'Option 2', + value: 1, + }, + ], + questionType: QuestionType.MultipleChoice, + userQuestionType: UserQuestionType.MultipleChoice, + questionConfig: { + recode_enabled: false, + mandatory: true, + slider: null, + }, + }, + ], +} + +export const sliderQuestionForm: Form = { + id: 'Tzr2NafTxJfR', + title: 'Form with slider question', + key: 'formWithSliderQuestion', + definition_id: '', + release_id: '', + questions: [ + { + id: 'XAgYxu_kbDPj', + title: 'This is slider question', + definition_id: 'Kzr2NafTxJfR', + key: 'thisIsSliderQuestion', + dataPointValueType: DataPointValueType.Number, + options: [], + questionType: QuestionType.Input, + userQuestionType: UserQuestionType.Slider, + questionConfig: { + recode_enabled: false, + mandatory: true, + slider: { + min: 0, + max: 10, + step_value: 1, + display_marks: true, + min_label: 'Min', + max_label: 'Max', + is_value_tooltip_on: true, + show_min_max_values: true, + }, + }, + }, + { + id: 'XAgYxu_kbDPj2', + title: 'This is slider question 2', + definition_id: 'Kzr2NafTxJfRa', + key: 'thisIsSliderQuestion2', + dataPointValueType: DataPointValueType.Number, + options: [], + questionType: QuestionType.Input, + userQuestionType: UserQuestionType.Slider, + questionConfig: { + recode_enabled: false, + mandatory: false, + slider: { + min: 0, + max: 10, + step_value: 1, + display_marks: false, + min_label: '', + max_label: '', + is_value_tooltip_on: false, + show_min_max_values: false, + }, + }, + }, + ], +} + +export const dateQuestionForm: Form = { + id: 'Tzr2NafTxJfR', + title: 'Form with date question', + key: 'formWithDateQuestion', + definition_id: '', + release_id: '', + questions: [ + { + id: 'XAgYxu_kbDPj', + title: 'This is date question', + definition_id: 'Kzr2NafTxJfR', + key: 'thisIsDateQuestion', + dataPointValueType: DataPointValueType.Date, + options: [], + questionType: QuestionType.Input, + userQuestionType: UserQuestionType.Date, + questionConfig: { + recode_enabled: false, + mandatory: true, + }, + }, + { + id: '5KMcDYtoz0rr', + title: 'This is number question', + definition_id: 'Kzr2NafTxJfR', + key: 'thisIsNumberQuestion', + dataPointValueType: DataPointValueType.Number, + options: [], + questionType: QuestionType.Input, + userQuestionType: UserQuestionType.Number, + questionConfig: { + recode_enabled: false, + mandatory: false, + slider: null, + }, + }, + ], +} diff --git a/src/components/Form/Form.spec.tsx b/src/components/Form/Form.spec.tsx new file mode 100644 index 00000000..916be3f9 --- /dev/null +++ b/src/components/Form/Form.spec.tsx @@ -0,0 +1,187 @@ +import { act, fireEvent, screen } from '@testing-library/react' +import { render, sliderQuestionForm, activity_mocks } from '../../../spec' +import { Form } from './Form' +import { Activity, AnswerInput, QuestionRuleResult } from './types' +import { ActivityObjectType, ActivityStatus } from '../Activities/types' +import { ActivityProvider } from '../Activities/context' +import { Form as FormType } from '../../hooks/useForm' + +const renderForm = (activity: Activity, form: FormType) => { + return render( + +
+ , + { + mocks: { + Query: { + form: () => ({ + success: true, + form, + }), + hostedSessionActivities: () => ({ + success: true, + activities: [activity], + }), + }, + Mutation: { + submitFormResponse: (): Activity => { + return { + ...activity, + status: ActivityStatus.Done, + } + }, + evaluateFormRules: ( + answers: Array + ): Array => { + return answers.map((answer) => ({ + question_id: answer.question_id, + rule_id: '', + satisfied: true, + })) + }, + }, + }, + } + ) +} + +const generateOptionalSliderForm = () => { + return { + ...sliderQuestionForm, + questions: [ + { + ...sliderQuestionForm.questions[0], + questionConfig: { + mandatory: false, + }, + }, + { + ...sliderQuestionForm.questions[1], + }, + ], + } +} + +describe('Form', () => { + describe('Slider Form', () => { + it('should display validation error when question is required and not touched', async () => { + const activity = activity_mocks.activity({ + object: { + id: sliderQuestionForm.id, + type: ActivityObjectType.Form, + name: sliderQuestionForm.title, + }, + }) + renderForm(activity, sliderQuestionForm) + + await screen.findByText('*') + await screen.findByText('This is slider question') + // make sure activities.form.question_required_error is not in the document first + expect( + screen.queryByText('activities.form.question_required_error') + ).toBe(null) + + const button = await screen.findByText( + 'activities.form.next_question_label' + ) + act(() => { + button.click() + }) + await screen.findByText('activities.form.question_required_error') + }), + it('should not display validation error when question is required and slider has been touched', async () => { + const activity = activity_mocks.activity({ + object: { + id: sliderQuestionForm.id, + type: ActivityObjectType.Form, + name: sliderQuestionForm.title, + }, + }) + renderForm(activity, sliderQuestionForm) + + await screen.findByText('*') + await screen.findByText('This is slider question') + // make sure activities.form.question_required_error is not in the document first + expect( + screen.queryByText('activities.form.question_required_error') + ).toBe(null) + + const slider: HTMLInputElement = await screen.findByTestId( + sliderQuestionForm.questions[0].id + ) + fireEvent.change(slider, { target: { value: 5 } }) + + const button = await screen.findByText( + 'activities.form.next_question_label' + ) + act(() => { + button.click() + }) + // make sure activities.form.question_required_error is still not in the document + expect( + screen.queryByText('activities.form.question_required_error') + ).toBe(null) + }), + it('should not display an error when question is not required and not touched', async () => { + const activity = activity_mocks.activity({ + object: { + id: sliderQuestionForm.id, + type: ActivityObjectType.Form, + name: sliderQuestionForm.title, + }, + }) + + renderForm(activity, generateOptionalSliderForm()) + + await screen.findByText('This is slider question') + // make sure activities.form.question_required_error is not in the document + expect(screen.queryByText('*')).toBe(null) + expect( + screen.queryByText('activities.form.question_required_error') + ).toBe(null) + + const button = await screen.findByText( + 'activities.form.next_question_label' + ) + act(() => { + button.click() + }) + // make sure activities.form.question_required_error is not in the document + expect( + screen.queryByText('activities.form.question_required_error') + ).toBe(null) + }), + it('should not display an error when question is not required and has been touched', async () => { + const activity = activity_mocks.activity({ + object: { + id: sliderQuestionForm.id, + type: ActivityObjectType.Form, + name: sliderQuestionForm.title, + }, + }) + renderForm(activity, generateOptionalSliderForm()) + + await screen.findByText('This is slider question') + // make sure activities.form.question_required_error is not in the document + expect(screen.queryByText('*')).toBe(null) + expect( + screen.queryByText('activities.form.question_required_error') + ).toBe(null) + + const slider = await screen.findByTestId( + sliderQuestionForm.questions[0].id + ) + fireEvent.change(slider, { target: { value: 5 } }) + const button = await screen.findByText( + 'activities.form.next_question_label' + ) + act(() => { + button.click() + }) + // make sure activities.form.question_required_error is not in the document + expect( + screen.queryByText('activities.form.question_required_error') + ).toBe(null) + }) + }) +}) diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx index 414a0914..b528872e 100644 --- a/src/components/Form/Form.tsx +++ b/src/components/Form/Form.tsx @@ -80,6 +80,9 @@ export const Form: FC = ({ activity }) => { ), no_options: t('activities.form.questions.select.no_options'), }, + slider: { + tooltip_guide: t('activities.form.questions.slider.tooltip_guide'), + }, } const button_labels = { diff --git a/src/hooks/useHostedSession/useHostedSession.ts b/src/hooks/useHostedSession/useHostedSession.ts index 7a48685b..c137f12e 100644 --- a/src/hooks/useHostedSession/useHostedSession.ts +++ b/src/hooks/useHostedSession/useHostedSession.ts @@ -63,10 +63,12 @@ export const useHostedSession = (): UseHostedSessionHook => { } useEffect(() => { - Sentry.setTags({ - session: router.query.sessionId as string, - api_endpoint: process.env.NEXT_PUBLIC_URL_ORCHESTRATION_API, - }) + if (!isNil(router)) { + Sentry.setTags({ + session: router.query.sessionId as string, + api_endpoint: process.env.NEXT_PUBLIC_URL_ORCHESTRATION_API, + }) + } }) useEffect(() => { diff --git a/yarn.lock b/yarn.lock index 2b724a78..269cdebb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -84,10 +84,10 @@ dependencies: node-fetch "^2.6.1" -"@awell-health/ui-library@0.1.35": - version "0.1.35" - resolved "https://registry.yarnpkg.com/@awell-health/ui-library/-/ui-library-0.1.35.tgz#cbd09e319eeff5f6c2e7b765fe002e87a281fd1d" - integrity sha512-6gDt5mb2QQC6p2KFNQF5i88vqAfUcfof6Qy5BPhl0541E5u0VrJIF4Ex7VXMlPxCCFokVqrCXUnrsuvtgYRw9A== +"@awell-health/ui-library@0.1.37": + version "0.1.37" + resolved "https://registry.yarnpkg.com/@awell-health/ui-library/-/ui-library-0.1.37.tgz#04b967920d242b5a88933b04a5bc64f91716573b" + integrity sha512-B37bM8Rs4mLRK2xqxcZs3aQEiVJY3Sqosz7xZH5mTJe3DNqIo49I6yH667Np5qg4g7O7EPIzFVX+YE3tO2RlNQ== dependencies: "@calcom/embed-react" "^1.0.10" "@heroicons/react" "^2.0.13"