UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

444 lines (390 loc) • 14.1 kB
import React from 'react' import {render, screen} from '@testing-library/react' import runAxeCheck from '@instructure/ui-axe-check' import update from 'immutability-helper' import omit from 'lodash/omit' import words from 'lodash/words' import difference from 'lodash/difference' import FillBlankResult from '../index' const defaultScoredData = { value: { fitb_uuid1: { resultScore: 1, userResponse: 'Christopfer', correctAnswer: 'Christopher', }, fitb_uuid2: { resultScore: 1, userResponse: '1492', }, fitb_uuid3: { value: { Spain: { resultScore: 1, userResponded: false, }, Espana: { resultScore: 1, userResponded: false, }, 'Kingdom of Spain': { resultScore: 1, userResponded: false, }, Portugal: { resultScore: 0, userResponded: true, }, }, }, fitb_uuid4: { resultScore: 1, userResponse: 'Asia', correctAnswer: 'Europe', }, fitb_uuid5: { resultScore: 1, correctAnswer: 'Atlantic', userResponse: 'Atlantic Ocean', }, fitb_uuid6: { value: { choice_uuid13_america: { resultScore: 1, userResponded: true, }, }, }, fitb_uuid7: { value: { choice_uuid13_confused: { resultScore: 1, userResponded: false, }, choice_uuid11_peaceful: { resultScore: 0, userResponded: true, }, }, }, }, } const props = { interactionData: { stemItems: [ {id: 'stem_uuid0', position: 1, type: 'blank', blankId: 'fitb_uuid1'}, {id: 'stem_uuid1', position: 2, type: 'text', value: ' Columbus sailed in '}, {id: 'stem_uuid2', position: 3, type: 'blank', blankId: 'fitb_uuid2'}, {id: 'stem_uuid3', position: 4, type: 'text', value: ' from the country of '}, {id: 'stem_uuid4', position: 5, type: 'blank', blankId: 'fitb_uuid3'}, {id: 'stem_uuid5', position: 6, type: 'text', value: ' on the continent of '}, {id: 'stem_uuid6', position: 7, type: 'blank', blankId: 'fitb_uuid4'}, {id: 'stem_uuid7', position: 8, type: 'text', value: ' across the '}, {id: 'stem_uuid8', position: 9, type: 'blank', blankId: 'fitb_uuid5'}, {id: 'stem_uuid9', position: 10, type: 'text', value: ' and found '}, {id: 'stem_uuid10', position: 11, type: 'blank', blankId: 'fitb_uuid6'}, {id: 'stem_uuid11', position: 12, type: 'text', value: '.'}, {id: 'stem_uuid12', position: 13, type: 'text', value: ' Where the people were '}, {id: 'stem_uuid13', position: 14, type: 'blank', blankId: 'fitb_uuid7'}, {id: 'stem_uuid14', position: 15, type: 'text', value: '.'}, ], blanks: [ { id: 'fitb_uuid1', answerType: 'openEntry', }, { id: 'fitb_uuid2', answerType: 'openEntry', }, { id: 'fitb_uuid3', answerType: 'openEntry', }, { id: 'fitb_uuid4', answerType: 'openEntry', }, { id: 'fitb_uuid5', answerType: 'openEntry', }, { id: 'fitb_uuid6', answerType: 'wordbank', }, { id: 'fitb_uuid7', choices: [ {id: 'choice_uuid11_peaceful', position: 1, itemBody: 'peaceful'}, {id: 'choice_uuid12_war-torn', position: 2, itemBody: 'war-torn'}, {id: 'choice_uuid13_confused', position: 3, itemBody: 'confused'}, ], answerType: 'dropdown', }, ], }, scoredData: defaultScoredData, } // A dumb helper for stripping some keys from the complex scoredData above const cloneWithoutKey = (obj, key) => { let objCopy = obj.constructor() let prop for (prop in obj) { if (obj.hasOwnProperty(prop) && prop !== key) { if (typeof obj[prop] === 'object') { objCopy[prop] = cloneWithoutKey(obj[prop], key) } else { objCopy[prop] = obj[prop] } } } return objCopy } describe('Fill in the Blank Result', () => { it('should render', () => { render(<FillBlankResult {...props} />) expect(document.body.textContent).toBeDefined() }) describe('stem items', () => { it('renders a Feedback wrapper for each blank item', () => { render(<FillBlankResult {...props} />) const showingText = document.body.textContent expect(showingText).toContain('Columbus sailed in') expect(showingText).toContain('and found') }) describe('blanks', () => { it('renders a Feedback wrapper for each blank item', () => { const {container} = render(<FillBlankResult {...props} />) const blankComponents = container.querySelectorAll('.fs-mask') expect(blankComponents.length).toBe(7) }) it('passes the correct props to the FeedbackWrapper', () => { render(<FillBlankResult {...props} />) // The first blank has status 'correct', userResponse 'Christopfer', correctAnswer 'Christopher' // status 'correct' means no "Correct Answer:" subtitle is shown expect(screen.getByText('Christopfer')).toBeInTheDocument() // correctAnswer 'Christopher' is only shown when status is not correct, // but it's still passed. Since status is 'correct', correctAnswer subtitle won't render. // Verify the user response is present expect(document.body.textContent).toContain('Christopfer') }) }) }) describe('scoredData variants', () => { it("renders if wordbank choice id is not in blank's choice list", () => { const newVal = { choice_uuid13_confused: { resultScore: 1, userResponded: false, }, // changed to match other blank's choice choice_uuid11_brazil: { resultScore: 0, userResponded: true, }, } const scoredDataWithWordbankChoice = update(defaultScoredData, { value: {fitb_uuid7: {value: {$set: newVal}}}, }) const {container} = render( <FillBlankResult {...props} scoredData={scoredDataWithWordbankChoice} />, ) expect(container.firstChild).toBeInTheDocument() }) it('renders if wordbank choice id does not match option', () => { const newVal = { choice_uuid13_confused: { resultScore: 1, userResponded: false, }, some_key_that_does_not_exist: { resultScore: 0, userResponded: true, }, } const scoredDataWithUnmatchedWordbankChoice = update(defaultScoredData, { value: {fitb_uuid7: {value: {$set: newVal}}}, }) const {container} = render( <FillBlankResult {...props} scoredData={scoredDataWithUnmatchedWordbankChoice} />, ) expect(container.firstChild).toBeInTheDocument() }) it('renders if wordbank was unanswered', () => { const newVal = { choice_uuid13_confused: { resultScore: 1, userResponded: false, }, } const scoredDataWithNoWordbankChoice = update(defaultScoredData, { value: {fitb_uuid7: {value: {$set: newVal}}}, }) const {container} = render( <FillBlankResult {...props} scoredData={scoredDataWithNoWordbankChoice} />, ) expect(container.firstChild).toBeInTheDocument() }) it('renders if value is completely missing', () => { const scoredDataWithNonResponse = update(defaultScoredData, { value: {$apply: val => omit(val, ['fitb_uuid3'])}, }) const {container} = render( <FillBlankResult {...props} scoredData={scoredDataWithNonResponse} />, ) expect(container.firstChild).toBeInTheDocument() }) it('renders with missing scores', () => { const scoredDataMissingScores = cloneWithoutKey(defaultScoredData, 'resultScore') const {container} = render( <FillBlankResult {...props} scoredData={scoredDataMissingScores} />, ) expect(container.firstChild).toBeInTheDocument() }) it('renders with no user responses', () => { const scoredDataMissingScores = cloneWithoutKey(defaultScoredData, 'userResponse') const {container} = render( <FillBlankResult {...props} scoredData={scoredDataMissingScores} />, ) expect(container.firstChild).toBeInTheDocument() }) }) describe('results display', () => { it('renders the correctAnswer if given and incorrect', () => { const withAtlanticWrong = update(defaultScoredData, { value: {fitb_uuid5: {resultScore: {$set: 0}}}, }) render(<FillBlankResult {...props} scoredData={withAtlanticWrong} />) const innerTextWords = words(document.body.textContent) const expectedWords = words('Correct Answer:Atlantic') expect(difference(expectedWords, innerTextWords).length).toBe(0) }) it('renders correctAnswer if multiple choice blank was unanswered', () => { const newVal = { choice_uuid13_confused: { resultScore: 1, userResponded: false, }, } const scoredDataWithNoWordbankChoice = update(defaultScoredData, { value: {fitb_uuid7: {value: {$set: newVal}}}, }) render(<FillBlankResult {...props} scoredData={scoredDataWithNoWordbankChoice} />) // When the dropdown blank is unanswered, the correct answer 'confused' should appear expect(document.body.textContent).toContain('confused') }) it('renders student response and correctAnswer if word bank blank is malformed', () => { const newVal = { correct: false, value: { America: { resultScore: 1, userResponded: false, }, Brazil: { resultScore: 0, userResponded: true, }, }, } const malformedScoredData = update(defaultScoredData, { value: {fitb_uuid6: {$set: newVal}}, }) render(<FillBlankResult {...props} scoredData={malformedScoredData} />) // The malformed wordbank blank should show userResponse 'Brazil' and correctAnswer 'America' expect(document.body.textContent).toContain('Brazil') expect(document.body.textContent).toContain('America') }) it('does not render rich blanks when richFITB is false, even after rerender with new interactionData', () => { const {rerender, queryByText} = render(<FillBlankResult {...props} richFITB={false} />) const updatedProps = { ...props, interactionData: { ...props.interactionData, blanks: [ ...props.interactionData.blanks, { id: 'fitb_uuid_extra', answerType: 'dropdown', choices: [{id: 'choice_1', position: 1, itemBody: 'rich-only choice'}], }, ], stemItems: [ ...props.interactionData.stemItems, {id: 'stem_uuid_extra', position: 99, type: 'blank', blankId: 'fitb_uuid_extra'}, ], }, } rerender(<FillBlankResult {...updatedProps} richFITB={false} />) expect(queryByText('rich-only choice')).toBeNull() }) describe('renderRichBlanks', () => { it('renders updated blanks when interactionData.blanks changes', async () => { const {queryByText, rerender} = render(<FillBlankResult {...props} />) const updatedProps = { ...props, interactionData: { ...props.interactionData, blanks: [ ...props.interactionData.blanks, { id: 'fitb_uuid8', answerType: 'openEntry', }, ], }, } rerender(<FillBlankResult {...updatedProps} />) expect(queryByText('Atlantic Ocean')).not.toBeNull() expect(queryByText('peaceful')).not.toBeNull() }) it('re-renders blanks when images load in richFITB', async () => { const propsWithImage = { ...props, itemBody: `<p><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" /> <span id="blank_fitb_uuid1"></span> Columbus sailed in <span id="blank_fitb_uuid2"></span></p>`, } const {queryByText, container} = render(<FillBlankResult {...propsWithImage} />) // Wait for image to load const img = container.querySelector('img') if (img && !img.complete) { await new Promise(resolve => { img.addEventListener('load', resolve, {once: true}) }) } // Give the component time to re-render after image load await new Promise(resolve => setTimeout(resolve, 50)) // Verify that the blank answers are rendered expect(queryByText('Christopfer')).not.toBeNull() }) it('observes DOM mutations for new blank elements in richFITB', async () => { const {container, queryByText} = render(<FillBlankResult {...props} />) // Verify initial render expect(queryByText('Christopfer')).not.toBeNull() // Simulate adding a new blank element to the DOM const itemBodyWrapper = container.querySelector('[data-automation="sdk-item-wrapper"]') if (itemBodyWrapper) { const newBlankElement = document.createElement('span') newBlankElement.id = 'blank_fitb_uuid8' itemBodyWrapper.appendChild(newBlankElement) // Give MutationObserver time to detect the change await new Promise(resolve => setTimeout(resolve, 50)) // The component should re-render expect(queryByText('Atlantic Ocean')).not.toBeNull() } }) }) }) describe('a11y tests', () => { it('should meet a11y standards', async () => { render(<FillBlankResult {...props} />) expect( await runAxeCheck(document.body, { ignores: ['region'], }), ).toBe(true) }) }) })