diff --git a/jest.config.cjs b/jest.config.cjs index b9c687d5e7..65dcd4e205 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -22,7 +22,7 @@ module.exports = { transformIgnorePatterns: [ 'node_modules/(?!(ol|@camptocamp/inkmap|@terrestris/*[a-z]*-util|d3-selection|color-*[a-z]*)|(rc-*[a-z]*)|' + 'filter-obj|query-string|decode-uri-component|split-on-first|shpjs/|rbush|quickselect|geostyler-openlayers-parser|' + - 'geostyler-style|geotiff|quick-lru|quickselect|jsts)' + 'geostyler-style|geotiff|pbf|quick-lru|quickselect|jsts)' ], setupFiles: [ '/jest/__mocks__/matchMediaMock.js' diff --git a/src/BackgroundLayerChooser/BackgroundLayerChooser.spec.tsx b/src/BackgroundLayerChooser/BackgroundLayerChooser.spec.tsx new file mode 100644 index 0000000000..1d591eed01 --- /dev/null +++ b/src/BackgroundLayerChooser/BackgroundLayerChooser.spec.tsx @@ -0,0 +1,145 @@ +import OlLayer from 'ol/layer/Layer'; +import OlLayerTile from 'ol/layer/Tile'; +import OlMap from 'ol/Map'; +import OlSourceOsm from 'ol/source/OSM'; +import OlLayerImage from 'ol/layer/Image'; +import OlLayerGroup from 'ol/layer/Group'; +import OlSourceImageWMS from 'ol/source/ImageWMS'; + +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; + +import TestUtil from '../Util/TestUtil'; +import BackgroundLayerChooser from './BackgroundLayerChooser'; + +describe('BackgroundLayerChooser', () => { + let map: OlMap; + let layers: OlLayer[]; + + beforeEach(() => { + layers = [ + new OlLayerTile({ + source: new OlSourceOsm() + }), + new OlLayerTile({ + source: new OlSourceOsm() + }) + ]; + layers[0].set('name', 'Layer 1'); + layers[1].set('name', 'Layer 2'); + map = TestUtil.createMap(); + map.addLayer(layers[0]); + map.addLayer(layers[1]); + }); + + afterEach(() => { + map?.dispose(); + layers = []; + }); + it('renders button and preview', () => { + const { container } = render(); + expect(container.querySelector('.bg-layer-chooser')).toBeInTheDocument(); + expect(container.querySelector('.bg-preview')).toBeInTheDocument(); + expect(container.querySelector('#overview-map')).toBeInTheDocument(); + }); + + it('shows layer options when button is clicked', () => { + const { container } = render(); + fireEvent.click(container.querySelector('.bg-layer-chooser') as HTMLElement); + waitFor(() => { + expect(container.querySelectorAll('.bg-preview').length).toBe(2); + }); + }); + + it('selects a layer and updates preview', () => { + const { container } = render(); + fireEvent.click(container.querySelector('.bg-layer-chooser') as HTMLElement); + waitFor(() => { + expect(container.querySelectorAll('.bg-preview').length).toBe(2); + fireEvent.click(container.querySelectorAll('.bg-preview')[1]); + }); + waitFor(() => { + expect(screen.getByText('Layer 1')).toBeInTheDocument(); + expect(screen.getByText('Layer 2')).toBeInTheDocument(); + }); + }); + + it('renders no background button if allowEmptyBackground is true', () => { + const { container } = render(); + fireEvent.click(container.querySelector('.bg-layer-chooser') as HTMLElement); + waitFor(() => { + expect(screen.getByText('No Background')).toBeInTheDocument(); + }); + }); + + it('selects no background and updates preview', () => { + const { container } = render(); + fireEvent.click(container.querySelector('.bg-layer-chooser') as HTMLElement); + waitFor(() => { + const noBgButtons = screen.getAllByText('No Background'); + fireEvent.click(noBgButtons[1]); + }); + waitFor(() => { + expect(screen.getAllByText('No Background').length).toBeGreaterThan(0); + }); + }); + + it('uses titleRenderer if provided', () => { + const titleRenderer = (layer: any) => Custom: {layer.get('name')}; + render(); + expect(screen.getByText('Custom: Layer 1')).toBeInTheDocument(); + }); + + it('shows noBackgroundTitle if provided', async () => { + const noBackgroundTitle = 'Empty'; + const { container } = render(); + fireEvent.click(container.querySelector('.change-bg-btn') as HTMLElement); + await waitFor(() => { + expect(screen.getByText(noBackgroundTitle)).toBeInTheDocument(); + }); + }); + + it('handles OlLayerImage as background', async () => { + const imageLayer = new OlLayerImage({ + source: new OlSourceImageWMS({ url: 'test', params: {} }) + }); + imageLayer.set('name', 'Image Layer'); + render(); + expect(screen.getByText('Image Layer')).toBeInTheDocument(); + }); + + it('handles OlLayerGroup as background', async () => { + const groupLayer = new OlLayerGroup({ + layers: [layers[0], layers[1]] + }); + groupLayer.set('name', 'Group Layer'); + render(); + expect(screen.getByText('Group Layer')).toBeInTheDocument(); + }); + + it('toggles layerOptionsVisible state on button click', async () => { + const { container } = render(); + const btn = container.querySelector('.change-bg-btn') as HTMLElement; + expect(container.querySelector('.layer-cards')).not.toBeInTheDocument(); + fireEvent.click(btn); + await waitFor(() => { + expect(container.querySelector('.layer-cards')).toBeInTheDocument(); + }); + fireEvent.click(btn); + await waitFor(() => { + expect(container.querySelector('.layer-cards')).not.toBeInTheDocument(); + }); + }); + + it('sets selectedLayer to undefined when no background is selected', async () => { + const { container } = render(); + fireEvent.click(container.querySelector('.change-bg-btn') as HTMLElement); + await waitFor(() => { + const noBgButton = screen.getByText('No Background'); + fireEvent.click(noBgButton); + }); + await waitFor(() => { + expect(screen.getAllByText('No Background').length).toBeGreaterThan(0); + }); + }); +}); diff --git a/src/BackgroundLayerPreview/BackgroundLayerPreview.spec.tsx b/src/BackgroundLayerPreview/BackgroundLayerPreview.spec.tsx new file mode 100644 index 0000000000..141c7d293a --- /dev/null +++ b/src/BackgroundLayerPreview/BackgroundLayerPreview.spec.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import OlLayerTile from 'ol/layer/Tile'; +import OlSourceOsm from 'ol/source/OSM'; +import OlMap from 'ol/Map'; + +import TestUtil from '../Util/TestUtil'; +import BackgroundLayerPreview from './BackgroundLayerPreview'; + +describe('BackgroundLayerPreview', () => { + let map: OlMap; + let layer: OlLayerTile; + + beforeEach(() => { + layer = new OlLayerTile({ source: new OlSourceOsm() }); + layer.set('name', 'Test Layer'); + map = TestUtil.createMap(); + map.addLayer(layer); + }); + + it('renders with default props', () => { + const { container } = render( + true} + /> + ); + expect(screen.getByText('Test Layer')).toBeInTheDocument(); + expect(container.querySelector('.map')).toBeInTheDocument(); + }); + + it('calls onClick when clicked', () => { + const onClick = jest.fn(); + render( + true} + /> + ); + fireEvent.click(screen.getByText('Test Layer').closest('.layer-preview')!); + waitFor(() => { + expect(onClick).toHaveBeenCalled(); + }); + }); + + it('calls backgroundLayerFilter on mouse events', () => { + const filter = jest.fn(() => true); + render( + + ); + const preview = screen.getByText('Test Layer').closest('.layer-preview')!; + fireEvent.mouseOver(preview); + fireEvent.mouseLeave(preview); + waitFor(() => { + expect(filter).toHaveBeenCalled(); + }); + }); + + it('renders custom title if titleRenderer is provided', () => { + render( + true} + titleRenderer={l => Custom: {l.get('name')}} + /> + ); + expect(screen.getByText('Custom: Test Layer')).toBeInTheDocument(); + }); + + it('applies selected class if activeLayer matches', () => { + render( + true} + /> + ); + expect(screen.getByText('Test Layer').closest('.selected')).toBeInTheDocument(); + }); + + it('renders with custom width and height', () => { + const { container } = render( + true} + width={200} + height={150} + /> + ); + const mapDiv = container.querySelector('.map'); + expect(mapDiv).toHaveStyle({ width: '200px', height: '150px' }); + }); +}); diff --git a/src/Button/DrawCutButton/DrawCutButton.spec.tsx b/src/Button/DrawCutButton/DrawCutButton.spec.tsx new file mode 100644 index 0000000000..731ae67ee3 --- /dev/null +++ b/src/Button/DrawCutButton/DrawCutButton.spec.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { render, fireEvent, screen, act, waitFor } from '@testing-library/react'; +import DrawCutButton from './DrawCutButton'; + +describe('DrawCutButton', () => { + it('renders and toggles pressed state', () => { + render(); + const btn = screen.getByRole('button'); + expect(btn).toBeInTheDocument(); + expect(btn).toHaveAttribute('aria-pressed', 'false'); + }); + + it('shows Popconfirm after drawing', async () => { + render(); + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 10)); + }); + waitFor(() => { + expect(screen.getByText('Perform cut?')).toBeInTheDocument(); + expect(screen.getByRole('button')).toBeDisabled(); + }); + }); + + it('calls onCutEnd when provided', async () => { + const onCutEnd = jest.fn(); + render(); + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 10)); + }); + waitFor(() => { + expect(screen.getByText('OK')).toBeInTheDocument(); + fireEvent.click(screen.getByText('OK')); + }); + waitFor(() => { + expect(screen.queryByText('Perform cut?')).not.toBeInTheDocument(); + }); + }); + + it('calls Popconfirm cancel', async () => { + render(); + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 10)); + }); + waitFor(() => { + expect(screen.getByText('Canceƶ')).toBeInTheDocument(); + fireEvent.click(screen.getByText('Cancel')); + }); + expect(screen.queryByText('Perform cut?')).not.toBeInTheDocument(); + }); + + it('applies custom className and popConfirmText', () => { + render( + + ); + const btn = screen.getByRole('button'); + expect(btn.className).toMatch(/my-custom/); + }); + + it('passes popConfirmProps to Popconfirm', async () => { + render( + + ); + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 10)); + }); + waitFor(() => { + expect(screen.getByText('Yes')).toBeInTheDocument(); + expect(screen.getByText('No')).toBeInTheDocument(); + }) + }); +}); diff --git a/src/FeatureLabelModal/FeatureLabelModal.spec.tsx b/src/FeatureLabelModal/FeatureLabelModal.spec.tsx new file mode 100644 index 0000000000..28d6b0a212 --- /dev/null +++ b/src/FeatureLabelModal/FeatureLabelModal.spec.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import OlFeature from 'ol/Feature'; +import OlGeomPoint from 'ol/geom/Point'; + +import FeatureLabelModal from './FeatureLabelModal'; + +describe('', () => { + let testFeature: OlFeature; + let onOk: jest.Mock; + let onCancel: jest.Mock; + + beforeEach(() => { + testFeature = new OlFeature({ + geometry: new OlGeomPoint([19.09, 1.09]) + }); + onOk = jest.fn(); + onCancel = jest.fn(); + }); + + it('is defined', () => { + expect(FeatureLabelModal).not.toBeUndefined(); + }); + + it('can be rendered', () => { + render( + + ); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /ok/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument(); + }); + + it('shows the label from the feature if present', () => { + testFeature.set('label', 'Test label'); + render( + + ); + expect(screen.getByText('Test label')).toBeInTheDocument(); + }); + + it('updates the label state when textarea changes', () => { + render( + + ); + const textarea = screen.getByRole('textbox'); + fireEvent.change(textarea, { target: { value: 'New label' } }); + expect((textarea as HTMLTextAreaElement).value).toBe('New label'); + }); + + it('calls onOk and sets the feature label when OK is clicked', () => { + render( + + ); + const textarea = screen.getByRole('textbox'); + fireEvent.change(textarea, { target: { value: 'MyLabel' } }); + fireEvent.click(screen.getByRole('button', { name: /ok/i })); + expect(testFeature.get('label')).toBe('MyLabel'); + expect(onOk).toHaveBeenCalled(); + }); + + it('calls onCancel when Cancel is clicked', () => { + render( + + ); + fireEvent.click(screen.getByRole('button', { name: /cancel/i })); + expect(onCancel).toHaveBeenCalled(); + }); + + it('splits label into lines if maxLabelLineLength is set', () => { + render( + + ); + const textarea = screen.getByRole('textbox'); + fireEvent.change(textarea, { target: { value: 'abcdef' } }); + fireEvent.click(screen.getByRole('button', { name: /ok/i })); + expect(testFeature.get('label')).toBe('abcd-\nef'); + }); + + it('returns null if feature is not provided', () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/src/Field/SearchField/SearchField.spec.tsx b/src/Field/SearchField/SearchField.spec.tsx new file mode 100644 index 0000000000..7eb6b8c713 --- /dev/null +++ b/src/Field/SearchField/SearchField.spec.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { DigitizeUtil } from '@terrestris/react-util/dist/Util/DigitizeUtil'; +import { renderInMapContext } from '@terrestris/react-util/dist/Util/rtlTestUtils'; +import OlFeature from 'ol/Feature'; +import OlPoint from 'ol/geom/Point'; +import OlMap from 'ol/Map'; +import OlView from 'ol/View'; +import { render, fireEvent, waitFor } from '@testing-library/react'; + +import { SearchField, SearchProps } from './SearchField'; + +describe('', () => { + const coord = [829729, 6708850]; + let map: OlMap; + let feature: OlFeature; + + const defaultProps: SearchProps = { + searchFunction: jest.fn(), + getValue: jest.fn().mockImplementation(feature => feature.properties.name), + onSelect: jest.fn(), + onSearchCompleted: jest.fn(), + getExtent: jest.fn().mockImplementation(() => [0, 0, 10, 10]), + className: 'test-class' + }; + + const renderComponent = (props: Partial = {}) => render(); + + + beforeEach(() => { + feature = new OlFeature({ + geometry: new OlPoint(coord), + someProp: 'test' + }); + + map = new OlMap({ + view: new OlView({ + center: coord, + zoom: 10 + }), + controls: [], + layers: [] + }); + + (DigitizeUtil.getDigitizeLayer(map)) + .getSource()?.addFeature(feature); + }); + + + it('is defined', () => { + expect(SearchField).not.toBeUndefined(); + }); + + it('can be rendered', () => { + const { container } = renderInMapContext(map, ); + + const button = container.querySelector('.ant-select-selection-search'); + expect(button).toBeVisible(); + }); + + it('calls setSearchTerm on input change', () => { + const { getByRole } = renderComponent(); + const input = getByRole('combobox'); + fireEvent.change(input, { target: { value: 'test' } }); + expect(input).toHaveValue('test'); + }); + + it('calls onSearchCompleted on search completion', () => { + const mockProps = { + ...defaultProps, + onSearchCompleted: jest.fn(), + }; + const searchCollection = { + type: 'FeatureCollection', + features: [feature as any] + }; + + const useSearchMock = jest.requireMock('@terrestris/react-util').useSearch; + useSearchMock.mockReturnValueOnce({ featureCollection: searchCollection, loading: false }); + + renderComponent(mockProps); + waitFor(() => { + expect(mockProps.onSearchCompleted).toHaveBeenCalledWith(searchCollection); + }); + }); +});