From 8ec872dabbc4266b7d59e35665ffa7937b83d008 Mon Sep 17 00:00:00 2001 From: sdobbert Date: Wed, 20 Nov 2024 14:04:50 +0100 Subject: [PATCH 1/5] test: unittest for AttributionsRow --- .../AttributionRow/index.spec.tsx | 113 ++++++++++++++++++ src/components/ToolMenu/Draw/index.spec.tsx | 29 +++++ 2 files changed, 142 insertions(+) create mode 100644 src/components/ToolMenu/Draw/Attributions/AttributionRow/index.spec.tsx 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..cc7acda5d --- /dev/null +++ b/src/components/ToolMenu/Draw/Attributions/AttributionRow/index.spec.tsx @@ -0,0 +1,113 @@ +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/index.spec.tsx b/src/components/ToolMenu/Draw/index.spec.tsx index 6508e42ea..c66d333c4 100644 --- a/src/components/ToolMenu/Draw/index.spec.tsx +++ b/src/components/ToolMenu/Draw/index.spec.tsx @@ -1,3 +1,32 @@ +import React from 'react'; + +import { + screen, + fireEvent, + cleanup, + waitFor +} from '@testing-library/react'; + +import { message } from 'antd'; + +import { Feature } from 'ol'; +import { Point } from 'ol/geom'; +import OlMap from 'ol/Map'; +import { fromLonLat } from 'ol/proj'; +import OlView from 'ol/View'; + +import { + Provider +} from 'react-redux'; + +import { DigitizeUtil } from '@terrestris/react-geo/dist/Util/DigitizeUtil'; + +import { renderInMapContext } from '@terrestris/react-geo/dist/Util/rtlTestUtils'; + +import { + store +} from '../../../store/store'; + import Draw from './index'; describe('', () => { From 204d236e0eecdf2ec9fdc20b4ddea841e82d242a Mon Sep 17 00:00:00 2001 From: sdobbert Date: Wed, 20 Nov 2024 14:12:53 +0100 Subject: [PATCH 2/5] fix: resolve conflict in Draw test --- src/components/ToolMenu/Draw/index.spec.tsx | 29 --------------------- 1 file changed, 29 deletions(-) diff --git a/src/components/ToolMenu/Draw/index.spec.tsx b/src/components/ToolMenu/Draw/index.spec.tsx index c66d333c4..6508e42ea 100644 --- a/src/components/ToolMenu/Draw/index.spec.tsx +++ b/src/components/ToolMenu/Draw/index.spec.tsx @@ -1,32 +1,3 @@ -import React from 'react'; - -import { - screen, - fireEvent, - cleanup, - waitFor -} from '@testing-library/react'; - -import { message } from 'antd'; - -import { Feature } from 'ol'; -import { Point } from 'ol/geom'; -import OlMap from 'ol/Map'; -import { fromLonLat } from 'ol/proj'; -import OlView from 'ol/View'; - -import { - Provider -} from 'react-redux'; - -import { DigitizeUtil } from '@terrestris/react-geo/dist/Util/DigitizeUtil'; - -import { renderInMapContext } from '@terrestris/react-geo/dist/Util/rtlTestUtils'; - -import { - store -} from '../../../store/store'; - import Draw from './index'; describe('', () => { From 5ebe3263ab172f41bddbe2f81e83ad0a68c7f044 Mon Sep 17 00:00:00 2001 From: sdobbert Date: Wed, 20 Nov 2024 14:27:34 +0100 Subject: [PATCH 3/5] fix: minor formatting issues --- .../AttributionRow/index.spec.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/ToolMenu/Draw/Attributions/AttributionRow/index.spec.tsx b/src/components/ToolMenu/Draw/Attributions/AttributionRow/index.spec.tsx index cc7acda5d..377f13331 100644 --- a/src/components/ToolMenu/Draw/Attributions/AttributionRow/index.spec.tsx +++ b/src/components/ToolMenu/Draw/Attributions/AttributionRow/index.spec.tsx @@ -31,9 +31,9 @@ describe('', () => { const autoComplete = document.querySelector('#field1_name'); - if(!autoComplete) { - return - } + if (!autoComplete) { + return; + }; expect(within(autoComplete.parentElement!).getByText('AttributionRow.keyPlaceholder')).toBeInTheDocument(); @@ -45,7 +45,6 @@ describe('', () => { expect(screen.getByText('Option2')).toBeInTheDocument(); }); - it('calls onChange when Input value changes', () => { const handleChange = jest.fn(); @@ -74,9 +73,9 @@ describe('', () => { const autoComplete = document.querySelector('#field1_name'); const input = screen.getByPlaceholderText('AttributionRow.valuePlaceholder'); - if(!autoComplete) { - return - } + if (!autoComplete) { + return; + }; fireEvent.blur(autoComplete); fireEvent.blur(input); @@ -89,8 +88,8 @@ describe('', () => { const mockFormData = { fields: { field1: { name: 'DuplicateKey' }, - field2: { name: 'DuplicateKey' }, - }, + field2: { name: 'DuplicateKey' } + } }; render( @@ -101,9 +100,9 @@ describe('', () => { const autoComplete = document.querySelector('#field1_name'); - if(!autoComplete) { - return - } + if (!autoComplete) { + return; + }; fireEvent.change(autoComplete, { target: { value: 'DuplicateKey' } }); fireEvent.blur(autoComplete); From f77e0dc811899d8c3a1e3b731debe077c6cafed9 Mon Sep 17 00:00:00 2001 From: sdobbert Date: Wed, 20 Nov 2024 16:13:31 +0100 Subject: [PATCH 4/5] test: unittest for DeleteAllButton --- .../Draw/DeleteAllButton/index.spec.tsx | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/components/ToolMenu/Draw/DeleteAllButton/index.spec.tsx 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(); + }); +}); From 75dfb8714bb08a041ccf154d49433b1327900d0e Mon Sep 17 00:00:00 2001 From: sdobbert Date: Tue, 7 Jan 2025 11:28:08 +0100 Subject: [PATCH 5/5] test: unittest for Draw --- package-lock.json | 32 ++++++ package.json | 19 ++-- src/components/ToolMenu/Draw/index.spec.tsx | 117 ++++++++++++++++++++ 3 files changed, 159 insertions(+), 9 deletions(-) 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/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(); + }); + }); +