UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

446 lines (352 loc) • 15.7 kB
import {vi} from 'vitest' import React from 'react' import {render, screen, waitFor} from '@testing-library/react' import userEvent from '@testing-library/user-event' import runAxeCheck from '@instructure/ui-axe-check' import HotSpotTake from '../index' const handleResponseUpdateStub = vi.fn() const props = { itemBody: 'item body', interactionData: { imageUrl: 'https://i.ytimg.com/vi/LgkAebhJ7iE/maxresdefault.jpg', hotspotsCount: 1, }, userResponse: { value: [], }, handleResponseUpdate: handleResponseUpdateStub, multipleHotSpotEnabled: true, } // Helper to get class component instance via ref trick. // HotSpotTake is a class component, so we can capture the instance. function renderWithRef(overrideProps = {}) { let instanceRef = null const WrappedComponent = class extends HotSpotTake { constructor(p) { super(p) instanceRef = this } } // Copy static properties WrappedComponent.propTypes = HotSpotTake.propTypes WrappedComponent.defaultProps = HotSpotTake.defaultProps const result = render(<WrappedComponent {...props} {...overrideProps} />) return {result, getInstance: () => instanceRef} } describe('HotSpot Take', () => { afterEach(() => { handleResponseUpdateStub.mockClear() }) describe('rendering', () => { it('renders ItemBodyWrapper and TargetContainer', () => { const {container} = render(<HotSpotTake {...props} />) // ItemBodyWrapper renders the itemBody text expect(screen.getByText('item body')).toBeInTheDocument() // TargetContainer renders an image const image = container.querySelector('img') expect(image).toBeInTheDocument() }) }) describe('Initial State', () => { it('initializes selectedCoordinates from userResponse.value if it is already set and multipleHotSpotEnabled is true', () => { const {getInstance} = renderWithRef({ userResponse: { value: [{x: 0.3, y: 0.3}], }, interactionData: { ...props.interactionData, hotspotsCount: 2, }, multipleHotSpotEnabled: true, }) const instance = getInstance() expect(instance.state.selectedCoordinates).toEqual([{x: 0.3, y: 0.3}]) }) it('initializes selectedCoordinates from userResponse.value if it is already set and multipleHotSpotEnabled is false', () => { const {getInstance} = renderWithRef({ userResponse: { value: {x: 0.3, y: 0.3}, }, interactionData: { ...props.interactionData, hotspotsCount: 2, }, multipleHotSpotEnabled: false, }) const instance = getInstance() expect(instance.state.selectedCoordinates).toEqual([{x: 0.3, y: 0.3}]) }) it('initializes selectedCoordinates as empty when userResponse.value is empty', () => { const {getInstance} = renderWithRef() const instance = getInstance() expect(instance.state.selectedCoordinates).toEqual([]) }) }) describe('Helpers', () => { it('#handleSetCoordinates - adds coordinates to selectedCoordinates and calls handleResponseUpdate', () => { const {getInstance} = renderWithRef() const instance = getInstance() const newState = {imageWidth: 700, imageHeight: 500} const newCoordinates = [{x: 10, y: 20}] instance.targetContainer.state = newState instance.handleSetCoordinates(newCoordinates) const args = handleResponseUpdateStub.mock.calls[0][0] expect(args[0].x).toBe(newCoordinates[0].x / newState.imageWidth) expect(args[0].y).toBe(newCoordinates[0].y / newState.imageHeight) expect(instance.state.selectedCoordinates.length).toBe(1) }) it('#handleSetCoordinates - does not add more than allowed hotspotsCount', () => { const {getInstance} = renderWithRef({ interactionData: { imageUrl: 'https://i.ytimg.com/vi/LgkAebhJ7iE/maxresdefault.jpg', hotspotsCount: 1, }, }) const instance = getInstance() const newState = {imageWidth: 700, imageHeight: 500} instance.targetContainer.state = newState // Add first coordinate instance.handleSetCoordinates([{x: 10, y: 20}]) expect(instance.state.selectedCoordinates.length).toBe(1) // Try adding a second coordinate instance.handleSetCoordinates([{x: 30, y: 40}]) expect(instance.state.selectedCoordinates.length).toBe(1) // Should still be 1, as limit is reached }) it('#handleSetCoordinates - ensures response is an object if multipleHotSpotEnabled is false', () => { const {getInstance} = renderWithRef({ ...props, multipleHotSpotEnabled: false, interactionData: {...props.interactionData, hotspotsCount: 1}, }) const instance = getInstance() const newState = {imageWidth: 700, imageHeight: 500} const newCoordinates = [{x: 10, y: 20}] instance.targetContainer.state = newState instance.handleSetCoordinates(newCoordinates) const args = handleResponseUpdateStub.mock.calls[0][0] expect(args).toEqual({ x: newCoordinates[0].x / newState.imageWidth, y: newCoordinates[0].y / newState.imageHeight, }) }) it('#handleResetSelections - clears selectedCoordinates and calls handleResponseUpdate', () => { const {getInstance} = renderWithRef() const instance = getInstance() instance.setState({selectedCoordinates: [{x: 0.1, y: 0.2}]}) instance.handleResetSelections() expect(instance.state.selectedCoordinates.length).toBe(0) expect(handleResponseUpdateStub).toHaveBeenCalledWith([]) }) it('#isAnyInputFocused - returns false when no input is focused', () => { const {getInstance} = renderWithRef() const instance = getInstance() const result = instance.isAnyInputFocused() expect(result).toBe(false) }) it('#isAnyInputFocused - returns true when an input is focused', () => { const {getInstance} = renderWithRef() // Mock input element const input = document.createElement('input') input.setAttribute('data-test-id', 'test-input') document.body.appendChild(input) input.focus() const instance = getInstance() const result = instance.isAnyInputFocused() expect(result).toBe(true) input.remove() }) it('#saveSelectedPoint - sets isSelectingPoint to false and saves point to selectedCoordinates', () => { const {getInstance} = renderWithRef() const instance = getInstance() instance.setState({keyboardCoordinates: {x: 0.5, y: 0.5}, isSelectingPoint: true}) instance.saveSelectedPoint() expect(instance.state.selectedCoordinates).toEqual([{x: 0.5, y: 0.5}]) expect(instance.state.keyboardCoordinates).toBeNull() expect(instance.state.isSelectingPoint).toBe(false) }) it('#saveSelectedPoint - ensures response is an object if multipleHotSpotEnabled is false', () => { const {getInstance} = renderWithRef({ ...props, multipleHotSpotEnabled: false, interactionData: {...props.interactionData, hotspotsCount: 1}, }) const instance = getInstance() instance.setState({keyboardCoordinates: {x: 0.5, y: 0.5}, isSelectingPoint: true}) instance.saveSelectedPoint() const args = handleResponseUpdateStub.mock.calls[0][0] expect(args).toEqual({x: 0.5, y: 0.5}) }) it('#canAddPoint - prevents adding a point when limit is reached', () => { const {getInstance} = renderWithRef({ interactionData: { imageUrl: 'https://i.ytimg.com/vi/LgkAebhJ7iE/maxresdefault.jpg', hotspotsCount: 1, }, }) const instance = getInstance() instance.setState({selectedCoordinates: [{x: 0.1, y: 0.1}]}) const result = instance.canAddPoint() expect(result).toBe(false) }) it('#startSelectingPoint - updates position, sets isSelectingPoint to true, and calls handleResponseUpdate', () => { const {getInstance} = renderWithRef() const instance = getInstance() instance.startSelectingPoint() expect(instance.state.keyboardCoordinates).toEqual({x: 0.5, y: 0.5}) expect(instance.state.isSelectingPoint).toBe(true) }) it('#movePosition - updates position and calls handleResponseUpdate', () => { const {getInstance} = renderWithRef() const instance = getInstance() instance.setState({keyboardCoordinates: {x: 0.5, y: 0.5}}) instance.movePosition('ArrowUp', 0.1) expect(instance.state.keyboardCoordinates).toEqual({x: 0.5, y: 0.4}) }) it('#movePosition - ensures position does not exceed bounds', () => { const {getInstance} = renderWithRef() const instance = getInstance() instance.setState({keyboardCoordinates: {x: 1, y: 1}}) instance.movePosition('ArrowDown', 0.1) expect(instance.state.keyboardCoordinates).toEqual({x: 1, y: 1}) }) }) describe('Key Handling', () => { describe('#isArrowKey', () => { it('returns true for valid arrow keys', () => { const {getInstance} = renderWithRef() const instance = getInstance() const validKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'] validKeys.forEach(key => { expect(instance.isArrowKey(key)).toBe(true) }) }) it('returns false for invalid keys', () => { const {getInstance} = renderWithRef() const instance = getInstance() const invalidKeys = ['Enter', 's', 'a', '', null] invalidKeys.forEach(key => { expect(instance.isArrowKey(key)).toBe(false) }) }) }) describe('#handleKeyDown', () => { it('does nothing when an input field is focused', () => { const {getInstance} = renderWithRef() const instance = getInstance() vi.spyOn(instance, 'isAnyInputFocused').mockReturnValue(true) instance.setState({focusedContainer: 'targetContainer'}) const startSelectingPointSpy = vi.spyOn(instance, 'startSelectingPoint') const event = new KeyboardEvent('keydown', {key: 's'}) instance.handleKeyDown(event) expect(startSelectingPointSpy).not.toHaveBeenCalled() }) it('calls startSelectingPoint when "s" key is pressed', () => { const {getInstance} = renderWithRef() const instance = getInstance() const startSelectingPointSpy = vi.spyOn(instance, 'startSelectingPoint') instance.setState({focusedContainer: 'targetContainer'}) const event = new KeyboardEvent('keydown', {key: 's'}) instance.handleKeyDown(event) expect(startSelectingPointSpy).toHaveBeenCalledOnce() }) it('calls saveSelectedPoint when "d" key is pressed and a point is being selected', () => { const {getInstance} = renderWithRef() const instance = getInstance() const saveSelectedPointSpy = vi.spyOn(instance, 'saveSelectedPoint') instance.setState({ isSelectingPoint: true, keyboardCoordinates: {x: 0.5, y: 0.5}, focusedContainer: 'targetContainer', }) const event = new KeyboardEvent('keydown', {key: 'd'}) instance.handleKeyDown(event) expect(saveSelectedPointSpy).toHaveBeenCalledOnce() }) it('calls handleResetSelections when "Backspace" or "Delete" key is pressed', () => { const {getInstance} = renderWithRef() const instance = getInstance() const handleResetSelectionsSpy = vi.spyOn(instance, 'handleResetSelections') instance.setState({focusedContainer: 'targetContainer'}) const backspaceEvent = new KeyboardEvent('keydown', {key: 'Backspace'}) instance.handleKeyDown(backspaceEvent) expect(handleResetSelectionsSpy).toHaveBeenCalledOnce() const deleteEvent = new KeyboardEvent('keydown', {key: 'Delete'}) instance.handleKeyDown(deleteEvent) expect(handleResetSelectionsSpy).toHaveBeenCalledTimes(2) }) it('calls movePosition when arrow keys are pressed', () => { const {getInstance} = renderWithRef() const instance = getInstance() const movePositionSpy = vi.spyOn(instance, 'movePosition') instance.setState({ keyboardCoordinates: {x: 0.5, y: 0.5}, focusedContainer: 'targetContainer', }) const arrowUpEvent = new KeyboardEvent('keydown', {key: 'ArrowUp'}) instance.handleKeyDown(arrowUpEvent) expect(movePositionSpy).toHaveBeenCalledWith('ArrowUp', expect.any(Number)) }) it('does nothing when arrow keys are pressed without keyboardCoordinates', () => { const {getInstance} = renderWithRef() const instance = getInstance() const movePositionSpy = vi.spyOn(instance, 'movePosition') instance.setState({keyboardCoordinates: null}) const event = new KeyboardEvent('keydown', {key: 'ArrowUp'}) instance.handleKeyDown(event) expect(movePositionSpy).not.toHaveBeenCalled() }) it('does not call any functions when focusedContainer is null', () => { const {getInstance} = renderWithRef() const instance = getInstance() instance.setState({focusedContainer: null}) const startSelectingPointSpy = vi.spyOn(instance, 'startSelectingPoint') const saveSelectedPointSpy = vi.spyOn(instance, 'saveSelectedPoint') const handleResetSelectionsSpy = vi.spyOn(instance, 'handleResetSelections') const movePositionSpy = vi.spyOn(instance, 'movePosition') const eventS = new KeyboardEvent('keydown', {key: 's'}) instance.handleKeyDown(eventS) const eventD = new KeyboardEvent('keydown', {key: 'd'}) instance.handleKeyDown(eventD) const backspaceEvent = new KeyboardEvent('keydown', {key: 'Backspace'}) instance.handleKeyDown(backspaceEvent) const arrowUpEvent = new KeyboardEvent('keydown', {key: 'ArrowUp'}) instance.handleKeyDown(arrowUpEvent) expect(startSelectingPointSpy).not.toHaveBeenCalled() expect(saveSelectedPointSpy).not.toHaveBeenCalled() expect(handleResetSelectionsSpy).not.toHaveBeenCalled() expect(movePositionSpy).not.toHaveBeenCalled() }) }) }) describe('keyboard shortcuts', () => { it('should render in a modal when clicking the keyboard shortcuts button', async () => { render(<HotSpotTake {...props} />) const showKeyboardShortcutsButton = screen.getByRole('button', { name: /show keyboard shortcuts/i, }) userEvent.click(showKeyboardShortcutsButton) expect(screen.queryByRole('dialog', {name: /keyboard shortcuts/i})).not.toBeNull() expect(screen.queryByRole('row', {name: /select a point/i})).not.toBeNull() expect(screen.queryByRole('row', {name: /save the position/i})).not.toBeNull() expect(screen.queryByRole('row', {name: /clear all selections/i})).not.toBeNull() expect(screen.queryByRole('row', {name: /move the point/i})).not.toBeNull() userEvent.click(screen.getAllByRole('button', {name: /close/i})[0]) await waitFor( () => { expect(screen.queryByRole('dialog', {name: /keyboard shortcuts/i})).toBeNull() }, {timeout: 5000}, ) }) }) describe('a11y tests', () => { it('should meet a11y standards', async () => { const {container} = render( <main> <HotSpotTake {...props} /> </main>, ) expect(await runAxeCheck(container)).toBe(true) }) }) })