@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
993 lines (840 loc) • 32 kB
JavaScript
import {vi} from 'vitest'
import React from 'react'
import DrawingContainer from '../index'
import {drawTypes} from '../../index'
import {fireEvent, render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import runAxeCheck from '@instructure/ui-axe-check'
import {getRootFiberFromDocument} from '../../../../../../tests/util/fiberUtils'
const onSetTypeStub = vi.fn()
const onRemoveImageStub = vi.fn()
const onModalOpenStub = vi.fn()
const onModalCloseStub = vi.fn()
const addPolygonPointStub = vi.fn()
const drawPolygonHotspotStub = vi.fn()
const handleCloseShapeStub = vi.fn()
const addSquareHotspotStub = vi.fn()
const addOvalHotspotStub = vi.fn()
const convertCoordinatesStub = vi.fn()
const handleExpandImageStub = vi.fn()
const OpenShortcutsModalStub = vi.fn()
const props = {
hotspots: [
{
type: 'square',
coordinates: [
{x: 0.6, y: 0.13},
{x: 0.7, y: 0.2},
],
id: 1,
},
{
type: 'oval',
coordinates: [
{x: 0.1, y: 0.3},
{x: 0.2, y: 0.2},
],
id: 2,
},
],
convertCoordinates: convertCoordinatesStub,
currentType: 'square',
drawTypes: drawTypes(),
onDropAccepted: () => {},
onModalOpen: onModalOpenStub,
onModalClose: onModalCloseStub,
onSetType: onSetTypeStub,
onRemoveImage: onRemoveImageStub,
url: 'https://i.ytimg.com/vi/LgkAebhJ7iE/maxresdefault.jpg',
currentHotspotId: 1,
onOpenShortcutsModal: vi.fn(),
onRemoveHotspot: vi.fn(),
setCanvasRef: vi.fn(),
}
// Helper to find the DrawingContainer class instance from the rendered fiber tree.
// DrawingContainer is wrapped by @withStyleOverrides HOC, so we walk the fiber tree
// to find the actual class instance.
function findInstance(containerOverride) {
const container = containerOverride || document.body
const rootFiber = getRootFiberFromDocument({container})
if (!rootFiber) return null
// Walk the fiber tree to find a class component with displayName 'DrawingContainer'
function findDrawingContainerInFiber(fiber) {
if (!fiber) return null
if (
fiber.stateNode &&
typeof fiber.stateNode === 'object' &&
fiber.stateNode.setState &&
fiber.stateNode.constructor?.displayName === 'DrawingContainer'
) {
return fiber.stateNode
}
let child = fiber.child
while (child) {
const found = findDrawingContainerInFiber(child)
if (found) return found
child = child.sibling
}
return null
}
return findDrawingContainerInFiber(rootFiber)
}
describe('Drawing Container', () => {
afterEach(() => {
onSetTypeStub.mockClear()
onRemoveImageStub.mockClear()
onModalOpenStub.mockClear()
onModalCloseStub.mockClear()
drawPolygonHotspotStub.mockClear()
addPolygonPointStub.mockClear()
handleCloseShapeStub.mockClear()
addSquareHotspotStub.mockClear()
addOvalHotspotStub.mockClear()
})
describe('rendering', () => {
it('renders header, actions and image', () => {
const {container} = render(<DrawingContainer {...props} />)
const image = container.querySelectorAll('img')
expect(screen.getByText("Draw the hot spot's shape")).toBeInTheDocument()
expect(image.length).toBe(1)
expect(image[0].src).toBe(props.url)
})
it('does not render header, actions and image if there is no url', () => {
const {container} = render(<DrawingContainer {...props} url={void 0} />)
const image = container.querySelectorAll('img')
const fileDrop = Array.from(container.querySelectorAll('input')).filter(
input => input.type === 'file',
)
expect(screen.queryByText("Draw the hot spot's shape")).not.toBeInTheDocument()
expect(image.length).toBe(0)
expect(fileDrop.length).toBe(1)
})
it('shows a custom label when type is polygon', () => {
render(
<DrawingContainer
{...props}
currentType="polygon"
hotspots={[
{
type: 'polygon',
coordinates: [
{x: 0.6, y: 0.13},
{x: 0.7, y: 0.2},
],
id: '1',
},
]}
/>,
)
expect(screen.getByText(/Double click to/)).toBeInTheDocument()
expect(screen.getByText(/close shape/)).toBeInTheDocument()
})
it('renders canvas', () => {
const {container} = render(<DrawingContainer {...props} />)
// The canvas elements are rendered for each hotspot
const canvases = container.querySelectorAll('canvas')
expect(canvases.length).toBeGreaterThan(0)
})
it('does not render canvas if there is no url', () => {
const {container} = render(<DrawingContainer {...props} url={void 0} />)
const canvases = container.querySelectorAll('canvas')
expect(canvases.length).toBe(0)
})
it('renders remove image button', () => {
render(<DrawingContainer {...props} />)
expect(screen.getByRole('button', {name: 'Remove Image'})).toBeInTheDocument()
})
it('renders expand image button', () => {
render(<DrawingContainer {...props} />)
expect(screen.getByRole('button', {name: 'Expand Image'})).toBeInTheDocument()
})
it('does not render canvas if there is no url (duplicate)', () => {
const {container} = render(<DrawingContainer {...props} url={void 0} />)
const canvases = container.querySelectorAll('canvas')
expect(canvases.length).toBe(0)
})
it('renders modal if there is url and isModalOpen is true', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
instance.setState({isModalOpen: true})
expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByText('Done')).toBeInTheDocument()
})
it('does not render modal if there is url and isModalOpen is false', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
instance.setState({isModalOpen: false})
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
})
})
describe('Errors', () => {
it('should show filedrop errors if errorsAreShowing is true and there is no url', () => {
const errorMsg = {type: 'error', text: 'something is wrong!'}
render(
<DrawingContainer
{...props}
errorsAreShowing={true}
url={void 0}
fileDropErrors={[errorMsg]}
/>,
)
expect(screen.getByText(errorMsg.text)).toBeInTheDocument()
})
it('should show canvas and type Errors if errorsAreShowing is true and there is an url', () => {
const canvasMsg = {type: 'error', text: 'canvas is wrong!'}
const typeMsg = {type: 'error', text: 'type is wrong!'}
render(
<DrawingContainer
{...props}
errorsAreShowing={true}
url="https://valid-url.com"
canvasErrors={[canvasMsg]}
typeErrors={[typeMsg]}
/>,
)
expect(screen.getByText(canvasMsg.text)).toBeInTheDocument()
expect(screen.getByText(typeMsg.text)).toBeInTheDocument()
})
})
describe('Helpers', () => {
describe('#componentDidMount', () => {
it('adds event listener', () => {
const addEventListenerSpy = vi.spyOn(window, 'addEventListener')
render(<DrawingContainer {...props} />)
const resizeCall = addEventListenerSpy.mock.calls.find(call => call[0] === 'resize')
expect(resizeCall).toBeDefined()
expect(resizeCall[0]).toBe('resize')
addEventListenerSpy.mockRestore()
})
it('sets image dimensions with a url', () => {
render(
<DrawingContainer
{...props}
url="https://i.ytimg.com/vi/LgkAebhJ7iE/maxresdefault.jpg"
/>,
)
const instance = findInstance()
expect(instance.state.imageWidth).toBeDefined()
expect(instance.state.imageHeight).toBeDefined()
})
})
describe('#componentDidUpdate', () => {
it('if url is lost, it focuses on the file drop input', () => {
const {rerender} = render(<DrawingContainer {...props} url="www.some_url.org" />)
rerender(<DrawingContainer {...props} url={void 0} />)
expect(document.activeElement.tagName).toBe('INPUT')
expect(document.activeElement.type).toBe('file')
})
it('does not reset focus if url remain undefined', () => {
const {rerender} = render(<DrawingContainer {...props} url={void 0} />)
rerender(<DrawingContainer {...props} url={void 0} currentType="oval" />)
expect(document.activeElement.tagName).not.toBe('INPUT')
expect(document.activeElement.type).not.toBe('file')
})
})
describe('#componentWillUnmount', () => {
it('removes event listener', () => {
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener')
const {unmount} = render(<DrawingContainer {...props} />)
unmount()
const resizeCall = removeEventListenerSpy.mock.calls.find(call => call[0] === 'resize')
expect(resizeCall).toBeDefined()
expect(resizeCall[0]).toBe('resize')
removeEventListenerSpy.mockRestore()
})
})
describe('#currentImageWidth', () => {
it('returns current image width', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const newState = {
imageWidth: 700,
imageHeight: 500,
}
instance.state = newState
const imageWidth = instance.currentImageWidth()
expect(imageWidth).toBe(newState.imageWidth)
})
})
describe('#currentImageHeight', () => {
it('returns current image width', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const newState = {
imageWidth: 700,
imageHeight: 500,
}
instance.state = newState
const imageHeight = instance.currentImageHeight()
expect(imageHeight).toBe(newState.imageHeight)
})
})
describe('transformCoordinates', () => {
it('should correctly transform coordinates based on image dimensions', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const coordinates = [
{x: 0.5, y: 0.5},
{x: 0.25, y: 0.75},
]
const newState = {
imageWidth: 700,
imageHeight: 500,
}
instance.state = newState
const imageWidth = instance.currentImageWidth()
const imageHeight = instance.currentImageHeight()
const expected = [
{x: 350, y: 250},
{x: 175, y: 375},
]
const result = instance.transformCoordinates(coordinates, imageWidth, imageHeight)
expect(result).toEqual(expected)
})
it('returns an empty array if coordinates are empty', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const result = instance.transformCoordinates([], 200, 200)
expect(result).toEqual([])
})
})
describe('updateCoordinates', () => {
const setupTest = (
coordinates,
key,
shiftKey,
altKey,
imageWidth = 100,
imageHeight = 100,
increment = 10,
) => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
return instance.updateCoordinates(
coordinates,
key,
increment,
imageWidth,
imageHeight,
shiftKey,
altKey,
)
}
const expectCoordinates = (result, expected) => {
expect(result).toEqual(expected)
}
it('updates bottomRight.x and prevents overlap when both shiftKey and altKey are pressed and ArrowLeft is used', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'ArrowLeft', true, true)
expectCoordinates(result, [
{x: 10, y: 10},
{x: 40, y: 50},
])
})
it('updates topLeft.x and prevents overlap when both shiftKey and altKey are pressed and ArrowRight is used', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'ArrowRight', true, true)
expectCoordinates(result, [
{x: 20, y: 10},
{x: 50, y: 50},
])
})
it('updates bottomRight.y and prevents overlap when both shiftKey and altKey are pressed and ArrowUp is used', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'ArrowUp', true, true)
expectCoordinates(result, [
{x: 10, y: 10},
{x: 50, y: 40},
])
})
it('updates topLeft.y and prevents overlap when both shiftKey and altKey are pressed and ArrowDown is used', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'ArrowDown', true, true)
expectCoordinates(result, [
{x: 10, y: 20},
{x: 50, y: 50},
])
})
it('moves topLeft.y upward within bounds when only shiftKey is pressed and ArrowUp is used', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'ArrowUp', true, false)
expectCoordinates(result, [
{x: 10, y: 0},
{x: 50, y: 50},
])
})
it('moves bottomRight.y downward within bounds when only shiftKey is pressed and ArrowDown is used', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'ArrowDown', true, false)
expectCoordinates(result, [
{x: 10, y: 10},
{x: 50, y: 60},
])
})
it('moves topLeft.x leftward within bounds when only shiftKey is pressed and ArrowLeft is used', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'ArrowLeft', true, false)
expectCoordinates(result, [
{x: 0, y: 10},
{x: 50, y: 50},
])
})
it('moves bottomRight.x rightward within bounds when only shiftKey is pressed and ArrowRight is used', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'ArrowRight', true, false)
expectCoordinates(result, [
{x: 10, y: 10},
{x: 60, y: 50},
])
})
it('moves both points upward within bounds when no modifiers are pressed and ArrowUp is used', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'ArrowUp', false, false)
expectCoordinates(result, [
{x: 10, y: 0},
{x: 50, y: 40},
])
})
it('moves both points downward within bounds when no modifiers are pressed and ArrowDown is used', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'ArrowDown', false, false)
expectCoordinates(result, [
{x: 10, y: 20},
{x: 50, y: 60},
])
})
it('moves both points leftward within bounds when no modifiers are pressed and ArrowLeft is used', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'ArrowLeft', false, false)
expectCoordinates(result, [
{x: 0, y: 10},
{x: 40, y: 50},
])
})
it('moves both points rightward within bounds when no modifiers are pressed and ArrowRight is used', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'ArrowRight', false, false)
expectCoordinates(result, [
{x: 20, y: 10},
{x: 60, y: 50},
])
})
it('does nothing for unsupported keys', () => {
const coordinates = [
{x: 10, y: 10},
{x: 50, y: 50},
]
const result = setupTest(coordinates, 'Enter', false, false)
expectCoordinates(result, coordinates)
})
})
describe('isArrowKey', () => {
it('returns true for valid arrow keys', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const validKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
validKeys.forEach(key => {
expect(instance.isArrowKey(key)).toBe(true)
})
})
it('returns false for invalid keys', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const invalidKeys = ['Enter', 'a', ' ', null]
invalidKeys.forEach(key => {
expect(instance.isArrowKey(key)).toBe(false)
})
})
})
describe('isLetterKey', () => {
it('returns true for valid letter keys', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const validKeys = ['b', 'B', 'd', 'D', 'o', 'O']
validKeys.forEach(key => {
expect(instance.isLetterKey(key)).toBe(true)
})
})
it('returns false for invalid keys', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const invalidKeys = ['ArrowUp', '1', 'Shift', null]
invalidKeys.forEach(key => {
expect(instance.isLetterKey(key)).toBe(false)
})
})
})
describe('#isBankOrOutcomesModalOpen', () => {
it('does not trigger "r" hotkey when sdk-add-to-bank-modal is present', () => {
const {container} = render(
<>
<div data-automation="sdk-add-to-bank-modal" />
<DrawingContainer {...props} />
</>,
)
const drawingContainer = container.querySelector('[data-automation="drawing-container"]')
if (drawingContainer) {
drawingContainer.focus()
userEvent.type(drawingContainer, 'r')
}
expect(addSquareHotspotStub).not.toHaveBeenCalled()
})
it('does not trigger "f" hotkey when sdk-add-to-bank-modal is present', () => {
const {container} = render(
<>
<div data-automation="sdk-add-to-bank-modal" />
<DrawingContainer {...props} />
</>,
)
const drawingContainer = container.querySelector('[data-automation="drawing-container"]')
if (drawingContainer) {
drawingContainer.focus()
userEvent.type(drawingContainer, 'f')
}
expect(handleExpandImageStub).not.toHaveBeenCalled()
})
it('does not trigger "i" hotkey when sdk-add-to-bank-modal is present', () => {
const {container} = render(
<>
<div data-automation="sdk-add-to-bank-modal" />
<DrawingContainer {...props} />
</>,
)
const drawingContainer = container.querySelector('[data-automation="drawing-container"]')
if (drawingContainer) {
drawingContainer.focus()
userEvent.type(drawingContainer, 'i')
}
expect(OpenShortcutsModalStub).not.toHaveBeenCalled()
})
})
})
describe('Actions', () => {
describe('#onWindowResize', () => {
it('updates the state with the new image width and height', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const onSetStateStub = vi.fn()
instance.setState = onSetStateStub
instance.updateSize()
instance.updateSize.flush()
const newState = onSetStateStub.mock.calls[0][0]
expect(newState.imageWidth).toBeDefined()
expect(newState.imageHeight).toBeDefined()
})
})
describe('#handleImageLoad', () => {
it('updates the state with the new image width and height', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const onSetStateStub = vi.fn()
instance.setState = onSetStateStub
instance.handleImageLoad({
target: {
offsetWidth: 100,
offsetHeight: 200,
},
})
const newState = onSetStateStub.mock.calls[0][0]
expect(newState.imageWidth).toBe(100)
expect(newState.imageHeight).toBe(200)
})
})
describe('#handleSelectDrawOption', () => {
describe('oval', () => {
it('calls onSetType with the correct arguments', () => {
render(<DrawingContainer {...props} />)
const button = screen.getAllByRole('button')[1]
userEvent.click(button)
const type = onSetTypeStub.mock.calls[0][0]
expect(type).toBe('oval')
})
})
describe('polygon', () => {
it('calls onSetType with the correct arguments', () => {
render(<DrawingContainer {...props} />)
const button = screen.getAllByRole('button')[2]
userEvent.click(button)
const type = onSetTypeStub.mock.calls[0][0]
expect(type).toBe('polygon')
})
})
})
describe('#handleExpandImage', () => {
it('notifies consumer that a full-screen modal is open', () => {
render(<DrawingContainer {...props} />)
const button = screen.getByRole('button', {name: 'Expand Image'})
userEvent.click(button)
expect(onModalOpenStub).toHaveBeenCalled()
})
it('opens modal when expand button is clicked', () => {
render(<DrawingContainer {...props} />)
const expandButton = screen.getByRole('button', {name: 'Expand Image'})
userEvent.click(expandButton)
const modal = screen.getByRole('dialog')
expect(modal).toBeInTheDocument()
})
it('returns early if outside drawing container', () => {
const {container} = render(
<>
<div data-automation="sdk-add-to-bank-modal" />
<DrawingContainer {...props} />
</>,
)
const drawingContainer = container.querySelector('[data-automation="drawing-container"]')
if (drawingContainer) {
drawingContainer.focus()
userEvent.type(drawingContainer, 'i')
}
expect(onModalOpenStub).not.toHaveBeenCalled()
})
})
describe('#handleCloseModal', () => {
it('notifies consumer that the full-screen modal is closed', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
instance.handleCloseModal()
expect(onModalCloseStub).toHaveBeenCalled()
})
it('updates component state', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const onSetStateStub = vi.fn()
instance.setState = onSetStateStub
instance.handleCloseModal()
const newState = onSetStateStub.mock.calls[0][0]
expect(newState.isModalOpen).toBe(false)
})
})
describe('#onRemoveImage', () => {
it('calls onRemoveImage prop', () => {
render(<DrawingContainer {...props} />)
const button = screen.getAllByRole('button')[4]
userEvent.click(button)
expect(onRemoveImageStub).toHaveBeenCalled()
})
})
describe('handleKeyPress', () => {
describe('with an uploaded image and drawing container is focused', () => {
const setupHandleKeyPressTest = ({
key,
shiftKey = false,
altKey = false,
imageUploaded = true,
hotspots = [],
} = {}) => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const spyMap = {
moveOrResizeHotspot: vi.spyOn(instance, 'moveOrResizeHotspot'),
addSquareHotspot: vi.spyOn(instance, 'addSquareHotspot'),
addOvalHotspot: vi.spyOn(instance, 'addOvalHotspot'),
drawPolygonHotspot: vi.spyOn(instance, 'drawPolygonHotspot'),
addPolygonPoint: vi.spyOn(instance, 'addPolygonPoint'),
handleExpandImage: vi.spyOn(instance, 'handleExpandImage'),
handleCloseModal: vi.spyOn(instance, 'handleCloseModal'),
deleteSelectedHotspot: vi.spyOn(instance, 'deleteSelectedHotspot'),
triggerFileUpload: vi.spyOn(instance, 'triggerFileUpload'),
}
vi.spyOn(HTMLElement.prototype, 'contains').mockReturnValue(true)
vi.spyOn(instance, 'isImageUploaded').mockReturnValue(imageUploaded)
const event = {key, shiftKey, altKey, preventDefault: vi.fn()}
instance.handleKeyPress(event)
return {spyMap, event}
}
it('calls moveOrResizeHotspot for arrow keys', () => {
const {spyMap, event} = setupHandleKeyPressTest({
key: 'ArrowUp',
shiftKey: true,
altKey: false,
imageUploaded: true,
hotspots: [{id: 1}],
})
expect(spyMap.moveOrResizeHotspot).toHaveBeenCalledWith('ArrowUp', true, false)
expect(event.preventDefault).toHaveBeenCalledTimes(2)
})
it('calls addSquareHotspot for "r" key', () => {
const {spyMap} = setupHandleKeyPressTest({key: 'r'})
expect(spyMap.addSquareHotspot).toHaveBeenCalledOnce()
})
it('calls addOvalHotspot for "o" key', () => {
const {spyMap} = setupHandleKeyPressTest({key: 'o'})
expect(spyMap.addOvalHotspot).toHaveBeenCalledOnce()
})
it('calls drawPolygonHotspot for "p" key', () => {
const {spyMap} = setupHandleKeyPressTest({key: 'p'})
expect(spyMap.drawPolygonHotspot).toHaveBeenCalledOnce()
})
it('calls deleteSelectedHotspot for "Delete" key', () => {
const {spyMap} = setupHandleKeyPressTest({key: 'Delete'})
expect(spyMap.deleteSelectedHotspot).toHaveBeenCalledOnce()
})
it('calls deleteSelectedHotspot for "Backspace" key', () => {
const {spyMap} = setupHandleKeyPressTest({key: 'Backspace'})
expect(spyMap.deleteSelectedHotspot).toHaveBeenCalledOnce()
})
it('calls handleExpandImage for "f" key', () => {
const {spyMap} = setupHandleKeyPressTest({key: 'f'})
expect(spyMap.handleExpandImage).toHaveBeenCalledOnce()
})
it('calls handleCloseModal for "Escape" key', () => {
const {spyMap} = setupHandleKeyPressTest({key: 'Escape'})
expect(spyMap.handleCloseModal).toHaveBeenCalledOnce()
})
it('calls addPolygonPoint for "Enter" key', () => {
const {spyMap} = setupHandleKeyPressTest({key: 'Enter'})
expect(spyMap.addPolygonPoint).toHaveBeenCalledOnce()
})
it('does nothing for an invalid key', () => {
const {spyMap} = setupHandleKeyPressTest({key: 'InvalidKey'})
Object.values(spyMap).forEach(spy => expect(spy).not.toHaveBeenCalled())
})
})
describe('when active element is text input or text area', () => {
it('returns early and prevents no actions', () => {
render(
<>
<input type="text" aria-label="Custom Input" />
<DrawingContainer {...props} />
</>,
)
const input = screen.getByRole('textbox', {
name: 'Custom Input',
})
userEvent.type(input, 'r')
expect(addSquareHotspotStub).not.toHaveBeenCalled()
})
})
})
})
describe('a11y tests', () => {
it('should meet a11y standards', async () => {
const {container} = render(
<main>
<DrawingContainer {...props} />
</main>,
)
expect(await runAxeCheck(container)).toBe(true)
})
})
// These keyboard shortcut tests dispatch events to window, but the component handles
// keyDown on its own element. The original sinon tests silently passed because
// sinon.stub().firstCall returns null, which passes toBeDefined(). The stubs were
// never actually invoked. The handleKeyPress tests above properly test this behavior.
describe('Keyboard Shortcuts', () => {
it.skip('calls drawPolygonHotspot when "p" key is pressed', () => {
render(<DrawingContainer {...props} />)
const event = new KeyboardEvent('keydown', {key: 'p'})
window.dispatchEvent(event)
expect(drawPolygonHotspotStub).toHaveBeenCalled()
})
it('calls addPolygonPoint when "Enter" key is pressed', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const spy = vi.spyOn(instance, 'addPolygonPoint')
fireEvent.keyDown(window, {key: 'Enter'})
expect(spy).toHaveBeenCalled()
})
it('calls handleCloseShape when "e" key is pressed', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const spy = vi.spyOn(instance, 'handleCloseShape')
fireEvent.keyDown(window, {key: 'e'})
expect(spy).toHaveBeenCalled()
})
it.skip('calls addSquareHotspot when "r" key is pressed', () => {
render(<DrawingContainer {...props} />)
const event = new KeyboardEvent('keydown', {key: 'r'})
window.dispatchEvent(event)
expect(addSquareHotspotStub).toHaveBeenCalled()
})
it.skip('calls addOvalHotspot when "o" key is pressed', () => {
render(<DrawingContainer {...props} />)
const event = new KeyboardEvent('keydown', {key: 'o'})
window.dispatchEvent(event)
expect(addOvalHotspotStub).toHaveBeenCalled()
})
})
describe('calculateCoordinates', () => {
it('should return correct coordinates for square', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const result = instance.calculateCoordinates('square', 200, 200)
const expected = [
{x: 75, y: 75},
{x: 125, y: 125},
]
expect(result).toEqual(expected)
})
it('should return correct coordinates for oval', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const result = instance.calculateCoordinates('oval', 200, 200)
const expected = [
{x: 50, y: 50},
{x: 150, y: 150},
]
expect(result).toEqual(expected)
})
it('should return correct coordinates for polygon', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const result = instance.calculateCoordinates('polygon', 200, 200)
const expected = [
{x: 75, y: 75},
{x: 125, y: 75},
]
expect(result).toEqual(expected)
})
it('should return an empty array for unknown shape type', () => {
render(<DrawingContainer {...props} />)
const instance = findInstance()
const result = instance.calculateCoordinates('triangle', 200, 200)
expect(result).toEqual([])
})
})
describe('addPolygonPoint', () => {
it('does not call convertCoordinates if currentPolygonCoordinates is null', () => {
render(<DrawingContainer {...props} tempHotspot={null} />)
const instance = findInstance()
instance.addPolygonPoint()
expect(addPolygonPointStub).not.toHaveBeenCalled()
})
})
})