props.onClick({ id: props.id, selected: !props.selected })}
+ />
+ );
+ };
+});
+
+jest.mock('../circle', () => {
+ return function Circle(props) {
+ return (
+
props.onClick({ id: props.id, selected: !props.selected })}
+ />
+ );
+ };
+});
+
+describe('Container', () => {
+ let defaultProps;
+
+ beforeEach(() => {
+ defaultProps = {
+ dimensions: { width: 800, height: 600 },
+ disabled: false,
+ hotspotColor: '#FF0000',
+ hoverOutlineColor: '#FFFF00',
+ selectedHotspotColor: '#00FF00',
+ imageUrl: 'http://example.com/image.png',
+ isEvaluateMode: false,
+ onSelectChoice: jest.fn(),
+ outlineColor: '#0000FF',
+ session: { answers: [] },
+ shapes: {
+ rectangles: [],
+ polygons: [],
+ circles: [],
+ },
+ strokeWidth: 5,
+ scale: 1,
+ showCorrect: false,
+ };
+ });
+
+ describe('rendering', () => {
+ it('should render without crashing', () => {
+ const { container } = render(
);
+ expect(container).toBeTruthy();
+ });
+
+ it('should render image when imageUrl is provided', () => {
+ const { getByAltText } = render(
);
+ const image = getByAltText('hotspot-image');
+ expect(image).toBeInTheDocument();
+ expect(image).toHaveAttribute('src', 'http://example.com/image.png');
+ });
+
+ it('should not render image when imageUrl is not provided', () => {
+ const { queryByAltText } = render(
);
+ expect(queryByAltText('hotspot-image')).not.toBeInTheDocument();
+ });
+
+ it('should render Stage with correct dimensions', () => {
+ const { getByTestId } = render(
);
+ const stage = getByTestId('stage');
+ expect(stage).toHaveAttribute('height', '605'); // 600 + strokeWidth
+ expect(stage).toHaveAttribute('width', '805'); // 800 + strokeWidth
+ });
+
+ it('should apply scale to dimensions', () => {
+ const { getByTestId } = render(
);
+ const stage = getByTestId('stage');
+ expect(stage).toHaveAttribute('height', '1205'); // (600 * 2) + strokeWidth
+ expect(stage).toHaveAttribute('width', '1605'); // (800 * 2) + strokeWidth
+ });
+
+ it('should render Layer inside Stage', () => {
+ const { getByTestId } = render(
);
+ const layer = getByTestId('layer');
+ expect(layer).toBeInTheDocument();
+ });
+ });
+
+ describe('rectangle shapes', () => {
+ it('should render rectangles', () => {
+ const props = {
+ ...defaultProps,
+ shapes: {
+ rectangles: [
+ { id: 'rect1', x: 10, y: 20, width: 100, height: 80, correct: false },
+ { id: 'rect2', x: 150, y: 200, width: 120, height: 90, correct: false },
+ ],
+ polygons: [],
+ circles: [],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ expect(getByTestId('rectangle-rect1')).toBeInTheDocument();
+ expect(getByTestId('rectangle-rect2')).toBeInTheDocument();
+ });
+
+ it('should mark rectangle as selected when in session answers', () => {
+ const props = {
+ ...defaultProps,
+ session: { answers: [{ id: 'rect1' }] },
+ shapes: {
+ rectangles: [
+ { id: 'rect1', x: 10, y: 20, width: 100, height: 80, correct: false },
+ ],
+ polygons: [],
+ circles: [],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ const rect = getByTestId('rectangle-rect1');
+ expect(rect).toHaveAttribute('data-selected', 'true');
+ });
+
+ it('should call onSelectChoice when rectangle is clicked', () => {
+ const onSelectChoice = jest.fn();
+ const props = {
+ ...defaultProps,
+ onSelectChoice,
+ shapes: {
+ rectangles: [
+ { id: 'rect1', x: 10, y: 20, width: 100, height: 80, correct: false },
+ ],
+ polygons: [],
+ circles: [],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ const rect = getByTestId('rectangle-rect1');
+ fireEvent.click(rect);
+
+ expect(onSelectChoice).toHaveBeenCalledWith({ id: 'rect1', selected: true });
+ });
+ });
+
+ describe('polygon shapes', () => {
+ it('should render polygons', () => {
+ const props = {
+ ...defaultProps,
+ shapes: {
+ rectangles: [],
+ polygons: [
+ {
+ id: 'poly1',
+ points: [{ x: 10, y: 10 }, { x: 100, y: 10 }, { x: 50, y: 100 }],
+ correct: false,
+ },
+ ],
+ circles: [],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ expect(getByTestId('polygon-poly1')).toBeInTheDocument();
+ });
+
+ it('should mark polygon as selected when in session answers', () => {
+ const props = {
+ ...defaultProps,
+ session: { answers: [{ id: 'poly1' }] },
+ shapes: {
+ rectangles: [],
+ polygons: [
+ {
+ id: 'poly1',
+ points: [{ x: 10, y: 10 }, { x: 100, y: 10 }, { x: 50, y: 100 }],
+ correct: false,
+ },
+ ],
+ circles: [],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ const polygon = getByTestId('polygon-poly1');
+ expect(polygon).toHaveAttribute('data-selected', 'true');
+ });
+
+ it('should call onSelectChoice when polygon is clicked', () => {
+ const onSelectChoice = jest.fn();
+ const props = {
+ ...defaultProps,
+ onSelectChoice,
+ shapes: {
+ rectangles: [],
+ polygons: [
+ {
+ id: 'poly1',
+ points: [{ x: 10, y: 10 }, { x: 100, y: 10 }, { x: 50, y: 100 }],
+ correct: false,
+ },
+ ],
+ circles: [],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ const polygon = getByTestId('polygon-poly1');
+ fireEvent.click(polygon);
+
+ expect(onSelectChoice).toHaveBeenCalledWith({ id: 'poly1', selected: true });
+ });
+ });
+
+ describe('circle shapes', () => {
+ it('should render circles', () => {
+ const props = {
+ ...defaultProps,
+ shapes: {
+ rectangles: [],
+ polygons: [],
+ circles: [
+ { id: 'circle1', x: 100, y: 100, radius: 50, correct: false },
+ ],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ expect(getByTestId('circle-circle1')).toBeInTheDocument();
+ });
+
+ it('should mark circle as selected when in session answers', () => {
+ const props = {
+ ...defaultProps,
+ session: { answers: [{ id: 'circle1' }] },
+ shapes: {
+ rectangles: [],
+ polygons: [],
+ circles: [
+ { id: 'circle1', x: 100, y: 100, radius: 50, correct: false },
+ ],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ const circle = getByTestId('circle-circle1');
+ expect(circle).toHaveAttribute('data-selected', 'true');
+ });
+
+ it('should call onSelectChoice when circle is clicked', () => {
+ const onSelectChoice = jest.fn();
+ const props = {
+ ...defaultProps,
+ onSelectChoice,
+ shapes: {
+ rectangles: [],
+ polygons: [],
+ circles: [
+ { id: 'circle1', x: 100, y: 100, radius: 50, correct: false },
+ ],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ const circle = getByTestId('circle-circle1');
+ fireEvent.click(circle);
+
+ expect(onSelectChoice).toHaveBeenCalledWith({ id: 'circle1', selected: true });
+ });
+ });
+
+ describe('evaluate mode', () => {
+ it('should show correctness when in evaluate mode', () => {
+ const props = {
+ ...defaultProps,
+ isEvaluateMode: true,
+ session: { answers: [{ id: 'rect1' }] },
+ shapes: {
+ rectangles: [
+ { id: 'rect1', x: 10, y: 20, width: 100, height: 80, correct: true },
+ ],
+ polygons: [],
+ circles: [],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ const rect = getByTestId('rectangle-rect1');
+ expect(rect).toHaveAttribute('data-iscorrect', 'true');
+ });
+
+ it('should show incorrect when selected but not correct', () => {
+ const props = {
+ ...defaultProps,
+ isEvaluateMode: true,
+ session: { answers: [{ id: 'rect1' }] },
+ shapes: {
+ rectangles: [
+ { id: 'rect1', x: 10, y: 20, width: 100, height: 80, correct: false },
+ ],
+ polygons: [],
+ circles: [],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ const rect = getByTestId('rectangle-rect1');
+ expect(rect).toHaveAttribute('data-iscorrect', 'false');
+ });
+
+ it('should show incorrect when not selected but correct', () => {
+ const props = {
+ ...defaultProps,
+ isEvaluateMode: true,
+ session: { answers: [] },
+ shapes: {
+ rectangles: [
+ { id: 'rect1', x: 10, y: 20, width: 100, height: 80, correct: true },
+ ],
+ polygons: [],
+ circles: [],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ const rect = getByTestId('rectangle-rect1');
+ expect(rect).toHaveAttribute('data-iscorrect', 'false');
+ });
+
+ it('should show correct when not selected and not correct', () => {
+ const props = {
+ ...defaultProps,
+ isEvaluateMode: true,
+ session: { answers: [] },
+ shapes: {
+ rectangles: [
+ { id: 'rect1', x: 10, y: 20, width: 100, height: 80, correct: false },
+ ],
+ polygons: [],
+ circles: [],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ const rect = getByTestId('rectangle-rect1');
+ expect(rect).toHaveAttribute('data-iscorrect', 'true');
+ });
+ });
+
+ describe('disabled state', () => {
+ it('should pass disabled prop to shapes', () => {
+ const props = {
+ ...defaultProps,
+ disabled: true,
+ shapes: {
+ rectangles: [
+ { id: 'rect1', x: 10, y: 20, width: 100, height: 80, correct: false },
+ ],
+ polygons: [],
+ circles: [],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ const rect = getByTestId('rectangle-rect1');
+ expect(rect).toHaveAttribute('data-disabled', 'true');
+ });
+ });
+
+ describe('mixed shapes', () => {
+ it('should render all types of shapes together', () => {
+ const props = {
+ ...defaultProps,
+ shapes: {
+ rectangles: [
+ { id: 'rect1', x: 10, y: 20, width: 100, height: 80, correct: false },
+ ],
+ polygons: [
+ {
+ id: 'poly1',
+ points: [{ x: 10, y: 10 }, { x: 100, y: 10 }, { x: 50, y: 100 }],
+ correct: false,
+ },
+ ],
+ circles: [
+ { id: 'circle1', x: 100, y: 100, radius: 50, correct: false },
+ ],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ expect(getByTestId('rectangle-rect1')).toBeInTheDocument();
+ expect(getByTestId('polygon-poly1')).toBeInTheDocument();
+ expect(getByTestId('circle-circle1')).toBeInTheDocument();
+ });
+
+ it('should handle multiple selections across different shape types', () => {
+ const props = {
+ ...defaultProps,
+ session: { answers: [{ id: 'rect1' }, { id: 'poly1' }, { id: 'circle1' }] },
+ shapes: {
+ rectangles: [
+ { id: 'rect1', x: 10, y: 20, width: 100, height: 80, correct: false },
+ ],
+ polygons: [
+ {
+ id: 'poly1',
+ points: [{ x: 10, y: 10 }, { x: 100, y: 10 }, { x: 50, y: 100 }],
+ correct: false,
+ },
+ ],
+ circles: [
+ { id: 'circle1', x: 100, y: 100, radius: 50, correct: false },
+ ],
+ },
+ };
+ const { getByTestId } = render(
);
+
+ expect(getByTestId('rectangle-rect1')).toHaveAttribute('data-selected', 'true');
+ expect(getByTestId('polygon-poly1')).toHaveAttribute('data-selected', 'true');
+ expect(getByTestId('circle-circle1')).toHaveAttribute('data-selected', 'true');
+ });
+ });
+
+ describe('showCorrect mode', () => {
+ it('should pass showCorrect prop to shapes in evaluate mode', () => {
+ const props = {
+ ...defaultProps,
+ isEvaluateMode: true,
+ showCorrect: true,
+ shapes: {
+ rectangles: [
+ { id: 'rect1', x: 10, y: 20, width: 100, height: 80, correct: true },
+ ],
+ polygons: [],
+ circles: [],
+ },
+ };
+ const { container } = render(
);
+ expect(container).toBeTruthy();
+ });
+ });
+
+ describe('getEvaluateText', () => {
+ it('should return correct text for correctly selected', () => {
+ const component = new Container(defaultProps);
+ const text = component.getEvaluateText(true, true);
+ expect(text).toBe('Correctly\nselected');
+ });
+
+ it('should return correct text for incorrectly selected', () => {
+ const component = new Container(defaultProps);
+ const text = component.getEvaluateText(false, true);
+ expect(text).toBe('Should not have\nbeen selected');
+ });
+
+ it('should return correct text for should have been selected', () => {
+ const component = new Container(defaultProps);
+ const text = component.getEvaluateText(true, false);
+ expect(text).toBe('Should have\nbeen selected');
+ });
+
+ it('should return null for correctly not selected', () => {
+ const component = new Container(defaultProps);
+ const text = component.getEvaluateText(false, false);
+ expect(text).toBeNull();
+ });
+ });
+
+ describe('correctness calculation', () => {
+ it('should calculate correctness properly', () => {
+ const component = new Container(defaultProps);
+
+ expect(component.correctness(true, true)).toBe(true);
+ expect(component.correctness(true, false)).toBe(false);
+ expect(component.correctness(false, true)).toBe(false);
+ expect(component.correctness(false, false)).toBe(true);
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle empty shapes object', () => {
+ const props = {
+ ...defaultProps,
+ shapes: {},
+ };
+ const { container } = render(
);
+ expect(container).toBeTruthy();
+ });
+
+ it('should handle null imageUrl', () => {
+ const props = {
+ ...defaultProps,
+ imageUrl: null,
+ };
+ const { queryByAltText } = render(
);
+ expect(queryByAltText('hotspot-image')).not.toBeInTheDocument();
+ });
+
+ it('should handle default scale value', () => {
+ const props = {
+ ...defaultProps,
+ scale: undefined,
+ };
+ const { getByTestId } = render(
);
+ const stage = getByTestId('stage');
+ expect(stage).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/hotspot/src/hotspot/__tests__/image-konva-tooltip.test.jsx b/packages/hotspot/src/hotspot/__tests__/image-konva-tooltip.test.jsx
new file mode 100644
index 0000000000..e32d89d9e8
--- /dev/null
+++ b/packages/hotspot/src/hotspot/__tests__/image-konva-tooltip.test.jsx
@@ -0,0 +1,510 @@
+import React from 'react';
+import { render, waitFor, fireEvent } from '@testing-library/react';
+import Konva from 'konva';
+import ImageComponent from '../image-konva-tooltip';
+
+Konva.isBrowser = false;
+
+jest.mock('react-konva', () => {
+ const React = require('react');
+ return {
+ Group: ({ children, ...props }) => React.createElement('div', { 'data-testid': 'group', ...props }, children),
+ Image: ({ onMouseEnter, onMouseLeave, ...props }) =>
+ React.createElement('div', {
+ 'data-testid': 'image',
+ onMouseEnter,
+ onMouseLeave,
+ ...props
+ }),
+ Text: (props) => React.createElement('div', { 'data-testid': 'text', ...props }),
+ Tag: (props) => React.createElement('div', { 'data-testid': 'tag', ...props }),
+ Label: ({ children, ...props }) => React.createElement('div', { 'data-testid': 'label', ...props }, children),
+ };
+});
+
+class MockImage {
+ constructor() {
+ this.src = '';
+ this._onload = null;
+ }
+
+ addEventListener(event, callback) {
+ if (event === 'load') {
+ this._onload = callback;
+ setTimeout(() => {
+ if (this._onload) {
+ this._onload();
+ }
+ }, 10);
+ }
+ }
+
+ removeEventListener() {
+ this._onload = null;
+ }
+}
+
+global.Image = MockImage;
+
+describe('ImageComponent', () => {
+ let defaultProps;
+
+ beforeEach(() => {
+ defaultProps = {
+ src: 'test-image.png',
+ x: 100,
+ y: 150,
+ tooltip: 'Test tooltip',
+ };
+ });
+
+ describe('rendering', () => {
+ it('should render without crashing', () => {
+ const { container } = render(
);
+ expect(container).toBeTruthy();
+ });
+
+ it('should render Group component', () => {
+ const { getByTestId } = render(
);
+ const group = getByTestId('group');
+ expect(group).toBeInTheDocument();
+ });
+
+ it('should render Image component', () => {
+ const { getByTestId } = render(
);
+ const image = getByTestId('image');
+ expect(image).toBeInTheDocument();
+ });
+
+ it('should have correct image dimensions', () => {
+ const { getByTestId } = render(
);
+ const image = getByTestId('image');
+ expect(image).toHaveAttribute('width', '20');
+ expect(image).toHaveAttribute('height', '20');
+ });
+
+ it('should have correct position', () => {
+ const { getByTestId } = render(
);
+ const image = getByTestId('image');
+ expect(image).toHaveAttribute('x', '100');
+ expect(image).toHaveAttribute('y', '150');
+ });
+ });
+
+ describe('image loading', () => {
+ it('should load image on mount', async () => {
+ const { getByTestId } = render(
);
+ const image = getByTestId('image');
+
+ await waitFor(() => {
+ expect(image).toHaveAttribute('image');
+ }, { timeout: 100 });
+ });
+
+ it('should reload image when src prop changes', async () => {
+ const { getByTestId, rerender } = render(
);
+
+ await waitFor(() => {
+ const image = getByTestId('image');
+ expect(image).toBeInTheDocument();
+ });
+
+ rerender(
);
+
+ await waitFor(() => {
+ const image = getByTestId('image');
+ expect(image).toBeInTheDocument();
+ });
+ });
+
+ it('should set image state after load', async () => {
+ const { getByTestId } = render(
);
+
+ await waitFor(() => {
+ const image = getByTestId('image');
+ expect(image).toBeInTheDocument();
+ }, { timeout: 100 });
+ });
+ });
+
+ describe('tooltip functionality', () => {
+ it('should not show tooltip initially', () => {
+ const { queryByTestId } = render(
);
+ const label = queryByTestId('label');
+ expect(label).not.toBeInTheDocument();
+ });
+
+ it('should show tooltip on mouse enter', async () => {
+ const { getByTestId, queryByTestId } = render(
);
+ const image = getByTestId('image');
+
+ // Initially no tooltip
+ expect(queryByTestId('label')).not.toBeInTheDocument();
+
+ // Simulate mouse enter
+ fireEvent.mouseEnter(image);
+
+ // Wait for state update
+ await waitFor(() => {
+ const label = queryByTestId('label');
+ expect(label).toBeInTheDocument();
+ });
+ });
+
+ it('should hide tooltip on mouse leave', async () => {
+ const { getByTestId, queryByTestId } = render(
);
+ const image = getByTestId('image');
+
+ // Show tooltip
+ fireEvent.mouseEnter(image);
+
+ await waitFor(() => {
+ expect(queryByTestId('label')).toBeInTheDocument();
+ });
+
+ // Hide tooltip
+ fireEvent.mouseLeave(image);
+
+ await waitFor(() => {
+ expect(queryByTestId('label')).not.toBeInTheDocument();
+ });
+ });
+
+ it('should render tooltip with correct text', async () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+
+ fireEvent.mouseEnter(image);
+
+ await waitFor(() => {
+ const text = getByTestId('text');
+ expect(text).toHaveAttribute('text', 'Custom tooltip');
+ });
+ });
+
+ it('should not show tooltip when tooltip prop is empty', async () => {
+ const { getByTestId, queryByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+
+ fireEvent.mouseEnter(image);
+
+ // Even after mouse enter, tooltip should not show if text is empty
+ await new Promise(resolve => setTimeout(resolve, 50));
+ expect(queryByTestId('label')).not.toBeInTheDocument();
+ });
+
+ it('should handle long tooltip text', async () => {
+ const longTooltip = 'This is a very long tooltip text that should still render correctly without breaking the component layout';
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+
+ fireEvent.mouseEnter(image);
+
+ await waitFor(() => {
+ const text = getByTestId('text');
+ expect(text).toHaveAttribute('text', longTooltip);
+ });
+ });
+ });
+
+ describe('positioning', () => {
+ it('should render at correct x position', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toHaveAttribute('x', '200');
+ });
+
+ it('should render at correct y position', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toHaveAttribute('y', '300');
+ });
+
+ it('should handle position at origin (0, 0)', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toHaveAttribute('x', '0');
+ expect(image).toHaveAttribute('y', '0');
+ });
+
+ it('should handle negative positions', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toHaveAttribute('x', '-10');
+ expect(image).toHaveAttribute('y', '-20');
+ });
+
+ it('should handle large position values', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toHaveAttribute('x', '1000');
+ expect(image).toHaveAttribute('y', '2000');
+ });
+
+ it('should position tooltip relative to image', async () => {
+ const { getByTestId } = render(
);
+ const image = getByTestId('image');
+
+ fireEvent.mouseEnter(image);
+
+ await waitFor(() => {
+ const label = getByTestId('label');
+ // Tooltip should be positioned at x - 30, y + 25
+ expect(label).toHaveAttribute('x', '70');
+ expect(label).toHaveAttribute('y', '175');
+ });
+ });
+ });
+
+ describe('image sources', () => {
+ it('should handle data URI image source', async () => {
+ const dataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toBeInTheDocument();
+ });
+
+ it('should handle relative path image source', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toBeInTheDocument();
+ });
+
+ it('should handle absolute URL image source', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toBeInTheDocument();
+ });
+
+ it('should handle SVG image source', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toBeInTheDocument();
+ });
+ });
+
+ describe('component lifecycle', () => {
+ it('should clean up event listener on unmount', () => {
+ const { unmount } = render(
);
+
+ expect(() => {
+ unmount();
+ }).not.toThrow();
+ });
+
+ it('should handle multiple mount/unmount cycles', () => {
+ const { unmount } = render(
);
+
+ unmount();
+
+ expect(() => {
+ render(
);
+ }).not.toThrow();
+ });
+
+ it('should handle rapid prop changes', async () => {
+ const { rerender, container } = render(
);
+
+ rerender(
);
+
+ await new Promise(resolve => setTimeout(resolve, 20));
+
+ rerender(
);
+
+ const image = container.querySelector('[data-testid="image"]');
+ expect(image).toHaveAttribute('x', '200');
+ expect(image).toHaveAttribute('y', '250');
+ });
+
+ it('should handle src changes during image load', async () => {
+ const { rerender, container } = render(
);
+
+ // Change src before image loads
+ rerender(
);
+
+ await waitFor(() => {
+ const image = container.querySelector('[data-testid="image"]');
+ expect(image).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle zero dimensions gracefully', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toHaveAttribute('x', '0');
+ expect(image).toHaveAttribute('y', '0');
+ });
+
+ it('should render with special characters in tooltip', async () => {
+ const specialTooltip = 'Test <>&"\'tooltip';
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+
+ fireEvent.mouseEnter(image);
+
+ await waitFor(() => {
+ const text = getByTestId('text');
+ expect(text).toHaveAttribute('text', specialTooltip);
+ });
+ });
+
+ it('should handle Unicode characters in tooltip', async () => {
+ const unicodeTooltip = 'Test ä½ å¥½ 🎉 tooltip';
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+
+ fireEvent.mouseEnter(image);
+
+ await waitFor(() => {
+ const text = getByTestId('text');
+ expect(text).toHaveAttribute('text', unicodeTooltip);
+ });
+ });
+
+ it('should handle missing tooltip gracefully', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toBeInTheDocument();
+ });
+ });
+
+ describe('tooltip styling', () => {
+ it('should render tooltip with Tag component', async () => {
+ const { getByTestId } = render(
);
+ const image = getByTestId('image');
+
+ fireEvent.mouseEnter(image);
+
+ await waitFor(() => {
+ const tag = getByTestId('tag');
+ expect(tag).toBeInTheDocument();
+ });
+ });
+
+ it('should render tooltip with correct styling attributes', async () => {
+ const { getByTestId } = render(
);
+ const image = getByTestId('image');
+
+ fireEvent.mouseEnter(image);
+
+ await waitFor(() => {
+ const tag = getByTestId('tag');
+ expect(tag).toHaveAttribute('fill', 'white');
+ expect(tag).toHaveAttribute('cornerRadius', '5');
+ expect(tag).toHaveAttribute('opacity', '0.9');
+ });
+ });
+
+ it('should render tooltip text with padding', async () => {
+ const { getByTestId } = render(
);
+ const image = getByTestId('image');
+
+ fireEvent.mouseEnter(image);
+
+ await waitFor(() => {
+ const text = getByTestId('text');
+ expect(text).toHaveAttribute('padding', '5');
+ });
+ });
+ });
+
+ describe('state management', () => {
+ it('should update showTooltip state on mouse events', async () => {
+ const { getByTestId, queryByTestId } = render(
);
+ const image = getByTestId('image');
+
+ // Initially no tooltip
+ expect(queryByTestId('label')).not.toBeInTheDocument();
+
+ // Show tooltip
+ fireEvent.mouseEnter(image);
+
+ await waitFor(() => {
+ expect(queryByTestId('label')).toBeInTheDocument();
+ });
+
+ // Hide tooltip
+ fireEvent.mouseLeave(image);
+
+ await waitFor(() => {
+ expect(queryByTestId('label')).not.toBeInTheDocument();
+ });
+ });
+
+ it('should update image state after load', async () => {
+ const { getByTestId } = render(
);
+
+ await waitFor(() => {
+ const image = getByTestId('image');
+ expect(image).toHaveAttribute('image');
+ }, { timeout: 100 });
+ });
+ });
+
+ describe('PropTypes validation', () => {
+ it('should accept all required props', () => {
+ const { getByTestId } = render(
);
+ const image = getByTestId('image');
+ expect(image).toBeInTheDocument();
+ });
+
+ it('should render with string src prop', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toBeInTheDocument();
+ });
+
+ it('should render with number x and y props', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toHaveAttribute('x', '100');
+ expect(image).toHaveAttribute('y', '200');
+ });
+
+ it('should render with string tooltip prop', () => {
+ const { getByTestId } = render(
+
+ );
+ const image = getByTestId('image');
+ expect(image).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/hotspot/src/hotspot/__tests__/polygon.test.jsx b/packages/hotspot/src/hotspot/__tests__/polygon.test.jsx
new file mode 100644
index 0000000000..b7f533e362
--- /dev/null
+++ b/packages/hotspot/src/hotspot/__tests__/polygon.test.jsx
@@ -0,0 +1,502 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import Konva from 'konva';
+import PolygonComponent from '../polygon';
+
+Konva.isBrowser = false;
+
+jest.mock('react-konva', () => {
+ const React = require('react');
+ return {
+ Line: ({ onClick, onTap, onMouseEnter, onMouseLeave, ...props }) => {
+ const handleClick = (e) => {
+ if (onClick) onClick(e);
+ if (onTap) onTap(e);
+ };
+ return React.createElement('div', {
+ 'data-testid': 'line',
+ onClick: handleClick,
+ onMouseEnter,
+ onMouseLeave,
+ ...props,
+ });
+ },
+ Rect: (props) => React.createElement('div', { 'data-testid': 'rect', ...props }),
+ Group: ({ children, ...props }) => React.createElement('div', { 'data-testid': 'group', ...props }, children),
+ };
+});
+
+jest.mock('../image-konva-tooltip', () => {
+ return function ImageComponent({ src, x, y, tooltip }) {
+ return
;
+ };
+});
+
+describe('PolygonComponent', () => {
+ let defaultProps;
+
+ beforeEach(() => {
+ defaultProps = {
+ id: 'polygon1',
+ points: [
+ { x: 10, y: 10 },
+ { x: 100, y: 10 },
+ { x: 100, y: 100 },
+ { x: 10, y: 100 },
+ ],
+ hotspotColor: '#FF0000',
+ selectedHotspotColor: '#00FF00',
+ outlineColor: '#0000FF',
+ hoverOutlineColor: '#FFFF00',
+ selected: false,
+ isCorrect: false,
+ isEvaluateMode: false,
+ disabled: false,
+ onClick: jest.fn(),
+ strokeWidth: 5,
+ scale: 1,
+ markAsCorrect: false,
+ showCorrectEnabled: false,
+ };
+ });
+
+ afterEach(() => {
+ document.body.style.cursor = 'default';
+ });
+
+ describe('rendering', () => {
+ it('should render without crashing', () => {
+ const { container } = render(
);
+ expect(container).toBeTruthy();
+ });
+
+ it('should parse points correctly for Konva', () => {
+ const { container } = render(
);
+ const line = container.querySelector('[data-testid="line"]');
+
+ expect(line).toHaveAttribute('points');
+ const points = line.getAttribute('points');
+ expect(points).toBe('10,10,100,10,100,100,10,100');
+ });
+
+ it('should render with hotspot color when not selected', () => {
+ const { container } = render(
);
+ const line = container.querySelector('[data-testid="line"]');
+
+ expect(line).toHaveAttribute('fill', '#FF0000');
+ });
+
+ it('should render with selected color when selected', () => {
+ const { container } = render(
);
+ const line = container.querySelector('[data-testid="line"]');
+
+ expect(line).toHaveAttribute('fill', '#00FF00');
+ });
+
+ it('should apply scale transform', () => {
+ const { getByTestId } = render(
);
+ const group = getByTestId('group');
+
+ expect(group).toHaveAttribute('scaleX', '1.5');
+ expect(group).toHaveAttribute('scaleY', '1.5');
+ });
+
+ it('should render with default scale of 1', () => {
+ const { getByTestId } = render(
);
+ const group = getByTestId('group');
+
+ expect(group).toHaveAttribute('scaleX', '1');
+ expect(group).toHaveAttribute('scaleY', '1');
+ });
+ });
+
+ describe('interactions', () => {
+ it('should call onClick when clicked', () => {
+ const onClick = jest.fn();
+ const { container } = render(
);
+ const line = container.querySelector('[data-testid="line"]');
+
+ fireEvent.click(line);
+
+ expect(onClick).toHaveBeenCalledWith({
+ id: 'polygon1',
+ selected: true,
+ selector: 'Mouse',
+ });
+ });
+
+ it('should toggle selection state on click', () => {
+ const onClick = jest.fn();
+ const { container, rerender } = render(
);
+ const line = container.querySelector('[data-testid="line"]');
+
+ fireEvent.click(line);
+
+ expect(onClick).toHaveBeenCalledWith({
+ id: 'polygon1',
+ selected: true,
+ selector: 'Mouse',
+ });
+
+ rerender(
);
+
+ const lineAfter = container.querySelector('[data-testid="line"]');
+ fireEvent.click(lineAfter);
+
+ expect(onClick).toHaveBeenCalledWith({
+ id: 'polygon1',
+ selected: false,
+ selector: 'Mouse',
+ });
+ });
+
+ it('should not call onClick when disabled', () => {
+ const onClick = jest.fn();
+ const { container } = render(
);
+ const line = container.querySelector('[data-testid="line"]');
+
+ fireEvent.click(line);
+
+ expect(onClick).not.toHaveBeenCalled();
+ });
+
+ it('should change cursor to pointer on mouse enter when not disabled', () => {
+ const { container } = render(
);
+ const line = container.querySelector('[data-testid="line"]');
+
+ fireEvent.mouseEnter(line);
+
+ expect(document.body.style.cursor).toBe('pointer');
+ });
+
+ it('should not change cursor when disabled', () => {
+ const { container } = render(
);
+ const line = container.querySelector('[data-testid="line"]');
+
+ fireEvent.mouseEnter(line);
+
+ expect(document.body.style.cursor).toBe('default');
+ });
+
+ it('should reset cursor to default on mouse leave', () => {
+ const { container } = render(
);
+ const line = container.querySelector('[data-testid="line"]');
+
+ fireEvent.mouseEnter(line);
+ fireEvent.mouseLeave(line);
+
+ expect(document.body.style.cursor).toBe('default');
+ });
+ });
+
+ describe('center calculation', () => {
+ it('should calculate center correctly for square', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toHaveAttribute('data-x', '50');
+ expect(icon).toHaveAttribute('data-y', '50');
+ });
+
+ it('should calculate center correctly for triangle', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toHaveAttribute('data-x', '50');
+ expect(icon).toHaveAttribute('data-y', '50');
+ });
+
+ it('should calculate center correctly for irregular polygon', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toHaveAttribute('data-x', '50');
+ expect(icon).toHaveAttribute('data-y', '45');
+ });
+ });
+
+ describe('hover styling', () => {
+ it('should show hover outline when hoverOutlineColor is provided', () => {
+ const { container } = render(
);
+ const line = container.querySelector('[data-testid="line"]');
+
+ fireEvent.mouseEnter(line);
+
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ expect(rects.length).toBeGreaterThan(0);
+ });
+
+ it('should render hover rect with correct dimensions', () => {
+ const { container } = render(
+
+ );
+
+ const line = container.querySelector('[data-testid="line"]');
+ fireEvent.mouseEnter(line);
+
+ const rect = container.querySelector('[data-testid="rect"]');
+ if (rect) {
+ expect(rect).toHaveAttribute('x', '10');
+ expect(rect).toHaveAttribute('y', '20');
+ expect(rect).toHaveAttribute('width', '100');
+ expect(rect).toHaveAttribute('height', '100');
+ }
+ });
+ });
+
+ describe('evaluate mode', () => {
+ it('should show correct icon when correctly selected in evaluate mode', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toBeInTheDocument();
+ });
+
+ it('should show wrong icon when incorrectly selected in evaluate mode', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toBeInTheDocument();
+ });
+
+ it('should show wrong icon when incorrectly not selected', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toBeInTheDocument();
+ });
+
+ it('should not show icon when correctly not selected in evaluate mode', () => {
+ const { queryByTestId } = render(
+
+ );
+
+ const icon = queryByTestId('icon-image');
+ expect(icon).not.toBeInTheDocument();
+ });
+
+ it('should show correct icon for showCorrect mode when correctly selected', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toBeInTheDocument();
+ });
+
+ it('should show correct icon for showCorrect mode when incorrectly not selected', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toBeInTheDocument();
+ });
+
+ it('should not show icon in showCorrect mode when correctly not selected', () => {
+ const { queryByTestId } = render(
+
+ );
+
+ const icon = queryByTestId('icon-image');
+ expect(icon).not.toBeInTheDocument();
+ });
+
+ it('should not show icon in showCorrect mode when incorrectly selected', () => {
+ const { queryByTestId } = render(
+
+ );
+
+ const icon = queryByTestId('icon-image');
+ expect(icon).not.toBeInTheDocument();
+ });
+
+ it('should show green outline when markAsCorrect is true', () => {
+ const { container } = render(
+
+ );
+
+ const line = container.querySelector('[data-testid="line"]');
+ expect(line).toHaveAttribute('stroke', 'green');
+ });
+
+ it('should show red outline when incorrect and not markAsCorrect', () => {
+ const { container } = render(
+
+ );
+
+ const line = container.querySelector('[data-testid="line"]');
+ expect(line).toHaveAttribute('stroke', 'red');
+ });
+
+ it('should display evaluate text in tooltip', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toHaveAttribute('data-tooltip', 'Well done!');
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle triangular polygon', () => {
+ const { container } = render(
+
+ );
+
+ const line = container.querySelector('[data-testid="line"]');
+ expect(line).toHaveAttribute('points', '50,0,100,100,0,100');
+ });
+
+ it('should handle complex polygon with many points', () => {
+ const points = [
+ { x: 10, y: 10 },
+ { x: 50, y: 5 },
+ { x: 90, y: 10 },
+ { x: 100, y: 50 },
+ { x: 90, y: 90 },
+ { x: 50, y: 100 },
+ { x: 10, y: 90 },
+ { x: 0, y: 50 },
+ ];
+
+ const { container } = render(
+
+ );
+
+ const line = container.querySelector('[data-testid="line"]');
+ expect(line).toHaveAttribute('points', '10,10,50,5,90,10,100,50,90,90,50,100,10,90,0,50');
+ });
+ });
+});
diff --git a/packages/hotspot/src/hotspot/__tests__/rectangle.test.jsx b/packages/hotspot/src/hotspot/__tests__/rectangle.test.jsx
new file mode 100644
index 0000000000..e865c9dbbe
--- /dev/null
+++ b/packages/hotspot/src/hotspot/__tests__/rectangle.test.jsx
@@ -0,0 +1,418 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import Konva from 'konva';
+import RectComponent from '../rectangle';
+
+Konva.isBrowser = false;
+
+jest.mock('react-konva', () => {
+ const React = require('react');
+ return {
+ Rect: ({ onClick, onTap, onMouseEnter, onMouseLeave, ...props }) => {
+ const handleClick = (e) => {
+ if (onClick) onClick(e);
+ if (onTap) onTap(e);
+ };
+ return React.createElement('div', {
+ 'data-testid': 'rect',
+ onClick: handleClick,
+ onMouseEnter,
+ onMouseLeave,
+ ...props,
+ });
+ },
+ Group: ({ children, ...props }) => React.createElement('div', { 'data-testid': 'group', ...props }, children),
+ };
+});
+
+jest.mock('../image-konva-tooltip', () => {
+ return function ImageComponent({ src, x, y, tooltip }) {
+ return
;
+ };
+});
+
+describe('RectComponent', () => {
+ let defaultProps;
+
+ beforeEach(() => {
+ defaultProps = {
+ id: 'rect1',
+ x: 10,
+ y: 20,
+ width: 100,
+ height: 80,
+ hotspotColor: '#FF0000',
+ selectedHotspotColor: '#00FF00',
+ outlineColor: '#0000FF',
+ hoverOutlineColor: '#FFFF00',
+ selected: false,
+ isCorrect: false,
+ isEvaluateMode: false,
+ disabled: false,
+ onClick: jest.fn(),
+ strokeWidth: 5,
+ scale: 1,
+ markAsCorrect: false,
+ showCorrectEnabled: false,
+ };
+ });
+
+ afterEach(() => {
+ document.body.style.cursor = 'default';
+ });
+
+ describe('rendering', () => {
+ it('should render without crashing', () => {
+ const { container } = render(
);
+ expect(container).toBeTruthy();
+ });
+
+ it('should render with correct dimensions', () => {
+ const { container } = render(
);
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+
+ expect(mainRect).toHaveAttribute('x', '10');
+ expect(mainRect).toHaveAttribute('y', '20');
+ expect(mainRect).toHaveAttribute('width', '100');
+ expect(mainRect).toHaveAttribute('height', '80');
+ });
+
+ it('should render with hotspot color when not selected', () => {
+ const { container } = render(
);
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+
+ expect(mainRect).toHaveAttribute('fill', '#FF0000');
+ });
+
+ it('should render with selected color when selected', () => {
+ const { container } = render(
);
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+
+ expect(mainRect).toHaveAttribute('fill', '#00FF00');
+ });
+
+ it('should apply scale transform', () => {
+ const { getByTestId } = render(
);
+ const group = getByTestId('group');
+
+ expect(group).toHaveAttribute('scaleX', '1.5');
+ expect(group).toHaveAttribute('scaleY', '1.5');
+ });
+
+ it('should render with default scale of 1', () => {
+ const { getByTestId } = render(
);
+ const group = getByTestId('group');
+
+ expect(group).toHaveAttribute('scaleX', '1');
+ expect(group).toHaveAttribute('scaleY', '1');
+ });
+ });
+
+ describe('interactions', () => {
+ it('should call onClick when clicked', () => {
+ const onClick = jest.fn();
+ const { container } = render(
);
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+
+ fireEvent.click(mainRect);
+
+ expect(onClick).toHaveBeenCalledWith({
+ id: 'rect1',
+ selected: true,
+ selector: 'Mouse',
+ });
+ });
+
+ it('should toggle selection state on click', () => {
+ const onClick = jest.fn();
+ const { container, rerender } = render(
);
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+
+ fireEvent.click(mainRect);
+
+ expect(onClick).toHaveBeenCalledWith({
+ id: 'rect1',
+ selected: true,
+ selector: 'Mouse',
+ });
+
+ rerender(
);
+
+ const rectsAfter = container.querySelectorAll('[data-testid="rect"]');
+ const mainRectAfter = rectsAfter[rectsAfter.length - 1];
+ fireEvent.click(mainRectAfter);
+
+ expect(onClick).toHaveBeenCalledWith({
+ id: 'rect1',
+ selected: false,
+ selector: 'Mouse',
+ });
+ });
+
+ it('should not call onClick when disabled', () => {
+ const onClick = jest.fn();
+ const { container } = render(
);
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+
+ fireEvent.click(mainRect);
+
+ expect(onClick).not.toHaveBeenCalled();
+ });
+
+ it('should change cursor to pointer on mouse enter when not disabled', () => {
+ const { container } = render(
);
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+
+ fireEvent.mouseEnter(mainRect);
+
+ expect(document.body.style.cursor).toBe('pointer');
+ });
+
+ it('should not change cursor when disabled', () => {
+ const { container } = render(
);
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+
+ fireEvent.mouseEnter(mainRect);
+
+ expect(document.body.style.cursor).toBe('default');
+ });
+
+ it('should reset cursor to default on mouse leave', () => {
+ const { container } = render(
);
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+
+ fireEvent.mouseEnter(mainRect);
+ fireEvent.mouseLeave(mainRect);
+
+ expect(document.body.style.cursor).toBe('default');
+ });
+ });
+
+ describe('hover styling', () => {
+ it('should show hover outline when hoverOutlineColor is provided', () => {
+ const { container } = render(
);
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+
+ fireEvent.mouseEnter(mainRect);
+
+ const rectsAfterHover = container.querySelectorAll('[data-testid="rect"]');
+ expect(rectsAfterHover.length).toBeGreaterThan(1);
+ });
+
+ it('should not show hover outline when selected', () => {
+ const { container } = render(
+
+ );
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+
+ fireEvent.mouseEnter(mainRect);
+
+ const hoverRect = container.querySelector('[stroke="#FFFF00"]');
+ if (hoverRect) {
+ expect(hoverRect).toHaveAttribute('stroke', 'transparent');
+ }
+ });
+ });
+
+ describe('evaluate mode', () => {
+ it('should show correct icon when correctly selected in evaluate mode', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toBeInTheDocument();
+ expect(icon).toHaveAttribute('data-src');
+ });
+
+ it('should show wrong icon when incorrectly selected in evaluate mode', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toBeInTheDocument();
+ });
+
+ it('should show wrong icon when incorrectly not selected', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toBeInTheDocument();
+ });
+
+ it('should not show icon when correctly not selected in evaluate mode', () => {
+ const { queryByTestId } = render(
+
+ );
+
+ const icon = queryByTestId('icon-image');
+ expect(icon).not.toBeInTheDocument();
+ });
+
+ it('should show correct icon for showCorrect mode when correctly selected', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toBeInTheDocument();
+ });
+
+ it('should show correct icon for showCorrect mode when incorrectly not selected', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toBeInTheDocument();
+ });
+
+ it('should not show icon in showCorrect mode when correctly not selected', () => {
+ const { queryByTestId } = render(
+
+ );
+
+ const icon = queryByTestId('icon-image');
+ expect(icon).not.toBeInTheDocument();
+ });
+
+ it('should not show icon in showCorrect mode when incorrectly selected', () => {
+ const { queryByTestId } = render(
+
+ );
+
+ const icon = queryByTestId('icon-image');
+ expect(icon).not.toBeInTheDocument();
+ });
+
+ it('should show green outline when markAsCorrect is true', () => {
+ const { container } = render(
+
+ );
+
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+ expect(mainRect).toHaveAttribute('stroke', 'green');
+ });
+
+ it('should show red outline when incorrect and not markAsCorrect', () => {
+ const { container } = render(
+
+ );
+
+ const rects = container.querySelectorAll('[data-testid="rect"]');
+ const mainRect = rects[rects.length - 1];
+ expect(mainRect).toHaveAttribute('stroke', 'red');
+ });
+
+ it('should display evaluate text in tooltip', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ expect(icon).toHaveAttribute('data-tooltip', 'Correct answer!');
+ });
+ });
+
+ describe('icon positioning', () => {
+ it('should position icon at center of rectangle', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const icon = getByTestId('icon-image');
+ // Icon should be centered: x + width/2 - 10, y + height/2 - 10
+ expect(icon).toHaveAttribute('data-x', '50'); // 10 + 100/2 - 10
+ expect(icon).toHaveAttribute('data-y', '50'); // 20 + 80/2 - 10
+ });
+ });
+});
diff --git a/packages/hotspot/src/hotspot/circle.jsx b/packages/hotspot/src/hotspot/circle.jsx
index 2be0d62816..1856c9f8ae 100644
--- a/packages/hotspot/src/hotspot/circle.jsx
+++ b/packages/hotspot/src/hotspot/circle.jsx
@@ -114,7 +114,6 @@ class CircleComponent extends React.Component {
onMouseEnter={this.handleMouseEnter}
x={x}
y={y}
- opacity={0.5}
/>
{isEvaluateMode && iconSrc ?
: null}
diff --git a/packages/hotspot/src/hotspot/polygon.jsx b/packages/hotspot/src/hotspot/polygon.jsx
index 1c1ea562e6..89168583d4 100644
--- a/packages/hotspot/src/hotspot/polygon.jsx
+++ b/packages/hotspot/src/hotspot/polygon.jsx
@@ -158,7 +158,6 @@ class PolygonComponent extends React.Component {
strokeWidth={useHoveredStyle && !selected ? 0 : outlineWidth}
onMouseLeave={this.handleMouseLeave}
onMouseEnter={this.handleMouseEnter}
- opacity={0.5}
cursor='pointer'
position='relative'
/>
diff --git a/packages/hotspot/src/hotspot/rectangle.jsx b/packages/hotspot/src/hotspot/rectangle.jsx
index 544a4f2987..43dc26f58e 100644
--- a/packages/hotspot/src/hotspot/rectangle.jsx
+++ b/packages/hotspot/src/hotspot/rectangle.jsx
@@ -131,7 +131,6 @@ class RectComponent extends React.Component {
strokeWidth={useHoveredStyle && !selected ? 0 : outlineWidth}
onMouseLeave={this.handleMouseLeave}
onMouseEnter={this.handleMouseEnter}
- opacity={0.5}
cursor="pointer"
/>
{isEvaluateMode && iconSrc ?
: null}