diff --git a/package-lock.json b/package-lock.json
index b070b3b74..2aa5e0387 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -85,6 +85,7 @@
"husky": "^9.1.6",
"ignore-loader": "^0.1.2",
"jest": "^29.7.0",
+ "jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.7.0",
"less": "^4.2.0",
"less-loader": "^12.2.0",
@@ -11327,6 +11328,12 @@
"node": ">=4"
}
},
+ "node_modules/cssfontparser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz",
+ "integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==",
+ "dev": true
+ },
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
@@ -16151,6 +16158,16 @@
}
}
},
+ "node_modules/jest-canvas-mock": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz",
+ "integrity": "sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==",
+ "dev": true,
+ "dependencies": {
+ "cssfontparser": "^1.2.1",
+ "moo-color": "^1.0.2"
+ }
+ },
"node_modules/jest-changed-files": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz",
@@ -19235,6 +19252,21 @@
"integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==",
"peer": true
},
+ "node_modules/moo-color": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz",
+ "integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "^1.1.4"
+ }
+ },
+ "node_modules/moo-color/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
"node_modules/mrmime": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",
diff --git a/package.json b/package.json
index 6e18f5a00..0e91ca85e 100644
--- a/package.json
+++ b/package.json
@@ -52,20 +52,20 @@
"antd": "^5.21.4",
"color": "^4.2.3",
"dotenv": "^16.4.5",
+ "geostyler": "^15.0.1",
"geostyler-openlayers-parser": "^5.0.0",
"geostyler-style": "^9.1.0",
- "geostyler": "^15.0.1",
- "i18next-browser-languagedetector": "^8.0.0",
"i18next": "^23.16.0",
+ "i18next-browser-languagedetector": "^8.0.0",
"js-md5": "^0.8.3",
"keycloak-js": "^26.0.0",
"normalize.css": "^8.0.1",
"ol": "^10.2.1",
+ "react": "^18.3.1",
"react-cookie-consent": "^9.0.0",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.3",
"react-redux": "^9.1.2",
- "react": "^18.3.1",
"shapefile.js": "^1.1.4"
},
"devDependencies": {
@@ -91,8 +91,8 @@
"@semantic-release/release-notes-generator": "^14.0.1",
"@stylistic/eslint-plugin": "^2.9.0",
"@swc/helpers": "^0.5.13",
- "@terrestris/eslint-config-typescript-react": "^3.0.0",
"@terrestris/eslint-config-typescript": "^7.0.0",
+ "@terrestris/eslint-config-typescript-react": "^3.0.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
@@ -107,25 +107,26 @@
"chokidar": "^4.0.1",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
+ "eslint": "^9.12.0",
"eslint-plugin-import": "^2.31.0",
- "eslint-plugin-react-refresh": "^0.4.12",
"eslint-plugin-react": "^7.37.1",
- "eslint": "^9.12.0",
+ "eslint-plugin-react-refresh": "^0.4.12",
"fs-extra": "^11.2.0",
"globals": "^15.11.0",
"husky": "^9.1.6",
"ignore-loader": "^0.1.2",
- "jest-environment-jsdom": "^29.7.0",
"jest": "^29.7.0",
- "less-loader": "^12.2.0",
+ "jest-canvas-mock": "^2.5.2",
+ "jest-environment-jsdom": "^29.7.0",
"less": "^4.2.0",
+ "less-loader": "^12.2.0",
"mini-css-extract-plugin": "^2.9.1",
"path-exists-cli": "2.0.0",
"react-refresh": "^0.14.2",
"semantic-release": "^24.1.2",
"style-loader": "^4.0.0",
- "typescript-eslint": "^8.9.0",
"typescript": "^5.6.3",
+ "typescript-eslint": "^8.9.0",
"webpack-merge": "^6.0.1"
},
"publishConfig": {
diff --git a/src/components/ToolMenu/Draw/Attributions/AttributionRow/index.spec.tsx b/src/components/ToolMenu/Draw/Attributions/AttributionRow/index.spec.tsx
new file mode 100644
index 000000000..377f13331
--- /dev/null
+++ b/src/components/ToolMenu/Draw/Attributions/AttributionRow/index.spec.tsx
@@ -0,0 +1,112 @@
+import React from 'react';
+
+import {
+ screen,
+ fireEvent,
+ render,
+ within
+} from '@testing-library/react';
+
+import { Form } from 'antd';
+
+import AttributionRow from './index';
+
+describe('', () => {
+ it('is defined', () => {
+ expect(AttributionRow).not.toBeUndefined();
+ });
+
+ it('can be rendered with placeholders and options', async () => {
+ render(
+
+ );
+
+ expect(screen.getByText('AttributionRow.keyPlaceholder')).toBeInTheDocument();
+ expect(screen.getByPlaceholderText('AttributionRow.valuePlaceholder')).toBeInTheDocument();
+
+ const autoComplete = document.querySelector('#field1_name');
+
+ if (!autoComplete) {
+ return;
+ };
+
+ expect(within(autoComplete.parentElement!).getByText('AttributionRow.keyPlaceholder')).toBeInTheDocument();
+
+ const input = screen.getByPlaceholderText('AttributionRow.valuePlaceholder');
+ expect(input).toBeInTheDocument();
+
+ fireEvent.focus(autoComplete);
+ expect(screen.getByText('Option1')).toBeInTheDocument();
+ expect(screen.getByText('Option2')).toBeInTheDocument();
+ });
+
+ it('calls onChange when Input value changes', () => {
+ const handleChange = jest.fn();
+
+ render(
+
+ );
+
+ const input = screen.getByPlaceholderText('AttributionRow.valuePlaceholder');
+ fireEvent.change(input, { target: { value: 'TestValue' } });
+
+ expect(handleChange).toHaveBeenCalledWith('TestValue');
+ });
+
+ it('validates the required fields', async () => {
+ render(
+
+ );
+
+ const autoComplete = document.querySelector('#field1_name');
+ const input = screen.getByPlaceholderText('AttributionRow.valuePlaceholder');
+
+ if (!autoComplete) {
+ return;
+ };
+
+ fireEvent.blur(autoComplete);
+ fireEvent.blur(input);
+
+ expect(await screen.findByText('AttributionRow.missingKey')).toBeInTheDocument();
+ expect(await screen.findByText('AttributionRow.missingValue')).toBeInTheDocument();
+ });
+
+ it('validates unique keys', async () => {
+ const mockFormData = {
+ fields: {
+ field1: { name: 'DuplicateKey' },
+ field2: { name: 'DuplicateKey' }
+ }
+ };
+
+ render(
+
+ );
+
+ const autoComplete = document.querySelector('#field1_name');
+
+ if (!autoComplete) {
+ return;
+ };
+
+ fireEvent.change(autoComplete, { target: { value: 'DuplicateKey' } });
+ fireEvent.blur(autoComplete);
+
+ expect(await screen.findByText('AttributionRow.keyInUse')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/ToolMenu/Draw/DeleteAllButton/index.spec.tsx b/src/components/ToolMenu/Draw/DeleteAllButton/index.spec.tsx
new file mode 100644
index 000000000..62c6ebe52
--- /dev/null
+++ b/src/components/ToolMenu/Draw/DeleteAllButton/index.spec.tsx
@@ -0,0 +1,179 @@
+import React from 'react';
+
+import {
+ fireEvent,
+ render,
+ cleanup
+} from '@testing-library/react';
+
+import {
+ Modal
+} from 'antd';
+
+import OlVectorLayer from 'ol/layer/Vector';
+import OlMap from 'ol/Map';
+import OlSourceVector from 'ol/source/Vector';
+import OlView from 'ol/View';
+
+import {
+ Provider
+} from 'react-redux';
+
+import { DigitizeUtil } from '@terrestris/react-util/dist/Util/DigitizeUtil';
+
+import { renderInMapContext } from '@terrestris/react-util/dist/Util/rtlTestUtils';
+
+import {
+ store
+} from '../../../../store/store';
+
+import DeleteAllButton from './index';
+
+let map: OlMap;
+
+jest.mock('antd', () => {
+ const originalModule = jest.requireActual('antd');
+ return {
+ ...originalModule,
+ Modal: {
+ confirm: jest.fn()
+ }
+ };
+});
+
+jest.mock('@terrestris/react-util/dist/Util/DigitizeUtil', () => ({
+ DigitizeUtil: {
+ getDigitizeLayer: jest.fn()
+ }
+}));
+
+const mockVectorLayer = new OlVectorLayer({
+ source: new OlSourceVector()
+});
+
+describe('', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ document.body.innerHTML = '';
+
+ map = new OlMap({
+ target: 'map',
+ view: new OlView({
+ zoom: 10
+ }),
+ controls: [],
+ layers: [mockVectorLayer]
+ });
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it('is defined', () => {
+ expect(DeleteAllButton).not.toBeUndefined();
+ });
+
+ it('renders without crashing', () => {
+ const { container } =
+ renderInMapContext(
+ map,
+
+
+
+ );
+ expect(container).toBeVisible();
+ expect(container.querySelector('button[type="button"]')).toBeInTheDocument();
+ });
+
+ it('uses the provided digitizeLayer when passed', () => {
+ renderInMapContext(
+ map,
+
+
+
+ );
+
+ const buttonElem = document.querySelector('button[type="button"]');
+
+ if (!buttonElem) {
+ return;
+ };
+
+ fireEvent.click(buttonElem);
+
+ expect(Modal.confirm).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: 'DeleteAllButton.deleteAll',
+ content: 'DeleteAllButton.confirmMessage',
+ icon: expect.anything(),
+ onOk: expect.any(Function)
+ })
+ );
+ });
+
+ it('uses the default digitizeLayer if no digitizeLayer is passed', () => {
+ renderInMapContext(
+ map,
+
+
+
+ );
+
+ const buttonElem = document.querySelector('button[type="button"]');
+
+ if (!buttonElem) {
+ return;
+ };
+
+ fireEvent.click(buttonElem);
+
+ expect(DigitizeUtil.getDigitizeLayer).toHaveBeenCalled();
+ expect(Modal.confirm).toHaveBeenCalled();
+ });
+
+ it('calls the source clear method on confirmation', () => {
+ renderInMapContext(
+ map,
+
+
+
+ );
+ const clearSpy = jest.spyOn(mockVectorLayer.getSource()!, 'clear');
+
+ const buttonElem = document.querySelector('button[type="button"]');
+
+ if (!buttonElem) {
+ return;
+ };
+
+ fireEvent.click(buttonElem);
+
+ const confirmArgs = (Modal.confirm as jest.Mock).mock.calls[0][0];
+ confirmArgs.onOk();
+
+ expect(clearSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not render if the map is not available', () => {
+ jest.mock('@terrestris/react-util/dist/Hooks/useMap/useMap', () => ({
+ useMap: jest.fn(() => null)
+ }));
+
+ const { container } = render();
+
+ expect(container.firstChild).toBeNull();
+ });
+});
diff --git a/src/components/ToolMenu/Draw/index.spec.tsx b/src/components/ToolMenu/Draw/index.spec.tsx
index 6508e42ea..ef8e48a0a 100644
--- a/src/components/ToolMenu/Draw/index.spec.tsx
+++ b/src/components/ToolMenu/Draw/index.spec.tsx
@@ -1,9 +1,126 @@
+import React from 'react';
+
+import {
+ cleanup,
+ fireEvent,
+ screen,
+ waitFor
+} from '@testing-library/react';
+
+import { setupJestCanvasMock } from 'jest-canvas-mock';
+
+import OlMap from 'ol/Map';
+import OlView from 'ol/View';
+import OlFeature from 'ol/Feature';
+import OlPoint from 'ol/geom/Point';
+
import Draw from './index';
+import { Provider } from 'react-redux';
+
+import { renderInMapContext } from '@terrestris/react-util/dist/Util/rtlTestUtils';
+import { DigitizeUtil } from '@terrestris/react-util/dist/Util/DigitizeUtil';
+
+import { store } from '../../../store/store';
+
describe('', () => {
+ let map: OlMap;
+ let feature: OlFeature;
+ global.URL.createObjectURL = jest.fn();
+
+ const coord = [829729, 6708850];
+
+ beforeEach(() => {
+ setupJestCanvasMock();
+
+ feature = new OlFeature({
+ geometry: new OlPoint(coord),
+ someProp: 'test'
+ });
+
+ map = new OlMap({
+ target: 'map',
+ view: new OlView({
+ center: coord,
+ zoom: 10
+ }),
+ controls: [],
+ layers: []
+ });
+ (DigitizeUtil.getDigitizeLayer(map))
+ .getSource()?.addFeature(feature);
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
it('is defined', () => {
expect(Draw).not.toBeUndefined();
});
+ it('can be rendered', () => {
+ const {
+ container
+ } = renderInMapContext(map,
+
+
+
+ );
+ expect(container).toBeVisible();
+ });
+
+ it('is rendered with all props', () => {
+ renderInMapContext(map,
+
+
+
+ );
+ expect(screen.getByText('Draw.point')).toBeVisible();
+ expect(screen.getByText('Draw.line')).toBeVisible();
+ expect(screen.getByText('Draw.polygon')).toBeVisible();
+ expect(screen.getByText('Draw.circle')).toBeVisible();
+ expect(screen.getByText('Draw.rectangle')).toBeVisible();
+ expect(screen.getByText('Draw.text')).toBeVisible();
+ expect(screen.getByText('Draw.modify')).toBeVisible();
+ expect(screen.getByText('Draw.upload')).toBeVisible();
+ expect(screen.getByText('Draw.delete')).toBeVisible();
+ expect(screen.getByText('StylingDrawer.openColorPalette')).toBeVisible();
+ });
+
+ it('imports data', async () => {
+ renderInMapContext(map,
+
+
+
+ );
+
+ await waitFor(() => {
+ fireEvent.click(screen.getByText('Draw.upload'));
+ });
+
+ const exportButton = screen.getByText('Draw.export')
+ expect(exportButton).toBeVisible();
+
+ await waitFor(() => {
+ fireEvent.click(exportButton);
+ });
+
+ expect(window.URL.createObjectURL).toHaveBeenCalled();
+ });
+
});
+