This is my sample text
", + requirementText: "My question requirements
", + questionText: "Custom Radio Button question", + guidanceText: "My question guidance
", + errors: { + __typename: "CustomQuestionErrors", + general: null, + guidanceText: null, + json: null, + migrationStatus: null, + pinnedQuestionId: null, + pinnedQuestionType: null, + questionText: null, + required: null, + requirementText: null, + sampleText: null, + sectionId: null, + sectionType: null, + templateCustomizationId: null, + useSampleTextAsDefault: null, + }, + migrationStatus: "OK", + pinnedQuestionId: null, + pinnedQuestionType: null, + required: true, + sectionId: 6203, + sectionType: "BASE", + templateCustomizationId: 8, + useSampleTextAsDefault: false, + }, +}; + +const mockTextAreaQuestion = { + customQuestion: { + ...mockRadioButtonQuestion.customQuestion, + id: 8, + questionText: "Custom Text Area question", + json: JSON.stringify({ + meta: { schemaVersion: "1.0" }, + type: "textArea", + attributes: { asRichText: true, cols: 20, rows: 20, maxLength: 1000, minLength: 0 }, + }), + useSampleTextAsDefault: true, + }, +}; + +const mockTextQuestion = { + customQuestion: { + ...mockRadioButtonQuestion.customQuestion, + id: 9, + questionText: "Custom Text question", + json: JSON.stringify({ + meta: { schemaVersion: "1.0" }, + type: "text", + attributes: { maxLength: 1000, minLength: 0, pattern: "^.+$" }, + }), + }, +}; + +const mockDateRangeQuestion = { + customQuestion: { + ...mockRadioButtonQuestion.customQuestion, + id: 10, + questionText: "Custom Date Range question", + json: JSON.stringify({ + meta: { schemaVersion: "1.0" }, + type: "dateRange", + columns: { + start: { label: "Start Date" }, + end: { label: "End Date" }, + }, + attributes: {}, + }), + }, +}; + +const mockAffiliationSearchQuestion = { + customQuestion: { + ...mockRadioButtonQuestion.customQuestion, + id: 11, + questionText: "Custom Affiliation Search question", + json: JSON.stringify({ + meta: { schemaVersion: "1.0" }, + type: "affiliationSearch", + attributes: { + label: "Institution", + help: "Search for your institution", + }, + graphQL: { + query: "query Affiliations($name: String!){ affiliations(name: $name) { items { id displayName uri } } }", + responseField: "affiliations.items", + variables: [{ name: "name", type: "string", label: "Search", minLength: 3 }], + answerField: "uri", + displayFields: [{ label: "Institution", propertyName: "displayName" }], + }, + }), + }, +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +let mockUpdateCustomQuestionFn: jest.Mock; +let mockRemoveCustomQuestionFn: jest.Mock; + +const setupMocks = (questionData = mockRadioButtonQuestion) => { + mockUseQuery.mockImplementation((document) => { + if (document === CustomQuestionDocument) { + /* eslint-disable @typescript-eslint/no-explicit-any */ + return { data: questionData, loading: false, error: null } as any; + } + return { data: null, loading: false, error: undefined }; + }); + + mockUpdateCustomQuestionFn = jest.fn().mockResolvedValue({ + data: { updateCustomQuestion: { errors: { general: null, questionText: null } } }, + }); + + mockRemoveCustomQuestionFn = jest.fn().mockResolvedValue({ + data: { removeCustomQuestion: { errors: { general: null } } }, + }); + + mockUseMutation.mockImplementation((document) => { + if (document === UpdateCustomQuestionDocument) { + /* eslint-disable @typescript-eslint/no-explicit-any */ + return [mockUpdateCustomQuestionFn, { loading: false, error: undefined }] as any; + } + if (document === RemoveCustomQuestionDocument) { + /* eslint-disable @typescript-eslint/no-explicit-any */ + return [mockRemoveCustomQuestionFn, { loading: false, error: undefined }] as any; + } + /* eslint-disable @typescript-eslint/no-explicit-any */ + return [jest.fn(), { loading: false, error: undefined }] as any; + }); +}; + +const setupSearchParams = (questionType = 'radioButtons') => { + mockUseSearchParams.mockReturnValue({ + get: (key: string) => { + const params: Record{t('descriptions.deleteWarning')}
+{QuestionEdit('descriptions.previewText')}
+{QuestionEdit('descriptions.bestPracticePara1')}
+{QuestionEdit('descriptions.bestPracticePara2')}
+{QuestionEdit('descriptions.bestPracticePara3')}
+ > +Your customizations to this Question will be removed from the template. This is not reversible.
-//{t('descriptions.previewText')}
-//{t('descriptions.bestPracticePara1')}
-//{t('descriptions.bestPracticePara2')}
-//{t('descriptions.bestPracticePara3')}
-//What data will you collect?
", + requirementText: "This is a requirement.
", + guidanceText: "Some base guidance here.
", + sampleText: "Sample answer text here.
", + useSampleTextAsDefault: false, + required: true, + json: null, + versionedTemplateId: 945, + ownerAffiliation: { + __typename: "Affiliation", + acronyms: ["NIH"], + displayName: "National Institutes of Health", + uri: "https://ror.org/01cwqze88", + name: "National Institutes of Health", + }, + }, +}; + +const mockPublishedQuestionNoOptionalFields = { + publishedQuestion: { + ...mockPublishedQuestion.publishedQuestion, + requirementText: null, + guidanceText: null, + sampleText: null, + }, +}; + +const mockMeData = { + me: { + __typename: "User", + affiliation: { + __typename: "Affiliation", + displayName: "Test University", + name: "Test University", + }, + }, +}; + +const mockExistingCustomization = { + questionCustomizationByVersionedQuestion: { + __typename: "QuestionCustomization", + id: 55, + guidanceText: "Existing guidance
", + sampleText: "Existing sample text
", + }, +}; + +const mockNoExistingCustomization = { + questionCustomizationByVersionedQuestion: null +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +let mockAddCustomizationFn: jest.Mock; +let mockUpdateCustomizationFn: jest.Mock; +let mockRemoveCustomizationFn: jest.Mock; + +const setupMocks = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + customizationData: { questionCustomizationByVersionedQuestion: any } = mockExistingCustomization, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + publishedQuestionData: { publishedQuestion: any } = mockPublishedQuestion +) => { + mockUseQuery.mockImplementation((document) => { + if (document === PublishedQuestionDocument) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { data: publishedQuestionData, loading: false, error: undefined } as any; + } + if (document === MeDocument) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { data: mockMeData, loading: false, error: undefined } as any; + } + if (document === QuestionCustomizationByVersionedQuestionDocument) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { data: customizationData, loading: false, error: undefined } as any; + } + return { data: null, loading: false, error: undefined }; + }); + + mockAddCustomizationFn = jest.fn().mockResolvedValue({ + data: { addQuestionCustomization: { id: 99, errors: null } }, + }); + + mockUpdateCustomizationFn = jest.fn().mockResolvedValue({ + data: { updateQuestionCustomization: { errors: { general: null, guidanceText: null, sampleText: null } } }, + }); + + mockRemoveCustomizationFn = jest.fn().mockResolvedValue({ + data: { removeQuestionCustomization: { errors: {} } }, + }); + + mockUseMutation.mockImplementation((document) => { + if (document === AddQuestionCustomizationDocument) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return [mockAddCustomizationFn, { loading: false }] as any; + } + if (document === UpdateQuestionCustomizationDocument) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return [mockUpdateCustomizationFn, { loading: false }] as any; + } + if (document === RemoveQuestionCustomizationDocument) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return [mockRemoveCustomizationFn, { loading: false }] as any; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return [jest.fn(), { loading: false }] as any; + }); +}; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("QuestionCustomizePage", () => { + let mockRouter: { push: jest.Mock }; + + beforeEach(() => { + setupMocks(); + HTMLElement.prototype.scrollIntoView = mockScrollIntoView; + mockScrollTo(); + + (useParams as jest.Mock).mockReturnValue({ + templateCustomizationId: '8', + versionedQuestionId: '101', + }); + + mockRouter = { push: jest.fn() }; + mockUseRouter.mockReturnValue(mockRouter); + + (useToast as jest.Mock).mockReturnValue({ add: jest.fn() }); + + window.tinymce = { init: jest.fn(), remove: jest.fn() }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + // ------------------------------------------------------------------------- + // Rendering + // ------------------------------------------------------------------------- + + describe("Rendering", () => { + it("should render the page heading", () => { + render({QuestionEdit('descriptions.previewText')}
+ {/** When modal closes, isPreviewOpen flips from true -> false, the key changes and React unmounts and remounts the inert QuestionView + * with a fresh TinyMCEEditor instance. This was needed because sometimes the TinyMCEEditor did not rerender after coming back from Preview*/} +{QuestionEdit('descriptions.bestPracticePara1')}
+{QuestionEdit('descriptions.bestPracticePara2')}
+{QuestionEdit('descriptions.bestPracticePara3')}
+ > +