UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

993 lines (840 loc) • 32 kB
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() }) }) })