From 91e9bc28842f0b295a6efd6c6e3b56bca2803966 Mon Sep 17 00:00:00 2001 From: Daniel Koch Date: Fri, 10 Jan 2025 13:43:48 +0100 Subject: [PATCH] chore: resolve merge conflicts for merge into main --- .github/workflows/on-pull-request.yml | 12 +- .github/workflows/on-push-main.yml | 2 +- CHANGELOG.md | 19 ++ jest.config.js | 9 +- jest.setup.js | 1 + package-lock.json | 29 +++ package.json | 1 + .../DeleteButton/index.spec.tsx | 88 ++++++- .../EditFeatureForm/index.spec.tsx | 99 +++++++- .../EditFeatureGeometryToolbar/index.spec.tsx | 228 +++++++++++++++++- 10 files changed, 461 insertions(+), 27 deletions(-) diff --git a/.github/workflows/on-pull-request.yml b/.github/workflows/on-pull-request.yml index 8878d43b4..e1e21b6b6 100644 --- a/.github/workflows/on-pull-request.yml +++ b/.github/workflows/on-pull-request.yml @@ -62,11 +62,18 @@ jobs: - name: Construct jest coverage comment input 💬 if: ${{ github.actor != 'dependabot[bot]' }} id: construct-input + env: + MULTI_LINES_TEXT: | + All, ./coverage/all/coverage-summary.json + only changed, ./coverage/changed/coverage-summary.json run: | if [ "${{ env.no_tests_found }}" == "true" ]; then echo "MULTIPLE_FILES=All, ./coverage/all/coverage-summary.json" >> $GITHUB_ENV else - echo "MULTIPLE_FILES=All, ./coverage/all/coverage-summary.json\nonly changed, ./coverage/changed/coverage-summary.json" >> $GITHUB_ENV + echo "MULTIPLE_FILES<> $GITHUB_ENV + echo "$MULTI_LINES_TEXT" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + cat $GITHUB_ENV fi - name: Jest Coverage Comment 💬 @@ -76,7 +83,8 @@ jobs: hide-comment: false create-new-comment: false hide-summary: false - multiple-files: ${{ env.MULTIPLE_FILES }} + multiple-files: | + ${{ env.MULTIPLE_FILES }} - name: Get Coverage Comment Id ➡️ if: ${{ env.no_tests_found == 'true' && github.actor != 'dependabot[bot]' }} diff --git a/.github/workflows/on-push-main.yml b/.github/workflows/on-push-main.yml index e94903152..693f3ca43 100644 --- a/.github/workflows/on-push-main.yml +++ b/.github/workflows/on-push-main.yml @@ -110,7 +110,7 @@ jobs: - name: Get shogun-gis-client version 🔖 run: | - echo "sonar.projectVersion=$(node -pe "require('./package.json').version")" >> ./sonar-project.properties + echo "sonar.projectVersion=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> ./sonar-project.properties - name: SonarQube Scan 🔬 uses: SonarSource/sonarqube-scan-action@v3.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index c8cb42002..a82f4d354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,25 @@ In addition webpack has been replaced by rspack. * update all dependencies to their latest versions ([3cfe3b3](https://github.com/terrestris/shogun-gis-client/commit/3cfe3b334a84cfd668825f34ace637bb414ecbf2)) +### Bugfixes + +* :lipstick: ([050ef7e](https://github.com/terrestris/shogun-gis-client/commit/050ef7ec6616f4014bcecbc5f30814c2b979e413)) +* fixes broken image symbol ([ef955f0](https://github.com/terrestris/shogun-gis-client/commit/ef955f0c3f4481efd0f12f27ee73fef9680857ee)) +* fixes bug in case no appID is passed ([95e9a23](https://github.com/terrestris/shogun-gis-client/commit/95e9a23f6c58cf8b8699ee24a03c3a692f297a2e)) + +## [8.11.1](https://github.com/terrestris/shogun-gis-client/compare/v8.11.0...v8.11.1) (2024-10-18) + + +### Dependencies + +* **deps:** bump SonarSource/sonarqube-scan-action from 2.3.0 to 3.0.0 ([8cbb4e6](https://github.com/terrestris/shogun-gis-client/commit/8cbb4e60ca811f3d32800c4795729a7a58616297)) + + +### Changes in configuration + +* enable releases from next and 8.x branches ([069e941](https://github.com/terrestris/shogun-gis-client/commit/069e941aa01cedd23da7d48d939eb9885c3fd234)) + + ### Bugfixes * :lipstick: ([050ef7e](https://github.com/terrestris/shogun-gis-client/commit/050ef7ec6616f4014bcecbc5f30814c2b979e413)) diff --git a/jest.config.js b/jest.config.js index 886b4542c..fda274b67 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,8 +11,11 @@ module.exports = { '^.+\\.tsx?$': 'babel-jest' }, testMatch: ['/src/**/?(*.)(spec).(j|t)s?(x)'], - testPathIgnorePatterns: ['/e2e-tests/'], - collectCoverageFrom: ['src/**/?!(*.ui)*.{tsx,jsx,ts,js}'], + testPathIgnorePatterns: ['/src/e2e-tests/'], + collectCoverageFrom: [ + '/src/**/*.{tsx,jsx,ts,js}', + '!/src/e2e-tests/**' + ], setupFilesAfterEnv: [ '/jest.setup.js', '/jest/matchMediaMock.js', @@ -34,6 +37,6 @@ module.exports = { 'default', '@casualbot/jest-sonar-reporter' ], - coverageReporters: ['json-summary', 'lcov'], + coverageReporters: ['json-summary', 'lcov', 'text'], coverageDirectory: 'coverage/all' }; diff --git a/jest.setup.js b/jest.setup.js index 34e966270..f2a663e6e 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,4 +1,5 @@ import '@testing-library/jest-dom'; +import 'jest-canvas-mock'; import { TextDecoder, TextEncoder diff --git a/package-lock.json b/package-lock.json index 02cf84a13..dd5d4e19e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,7 @@ "husky": "^9.1.7", "ignore-loader": "^0.1.2", "jest": "^29.7.0", + "jest-canvas-mock": "^2.5.2", "jest-environment-jsdom": "^29.7.0", "less": "^4.2.1", "less-loader": "^12.2.0", @@ -11475,6 +11476,13 @@ "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, + "license": "MIT" + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -16644,6 +16652,17 @@ } } }, + "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, + "license": "MIT", + "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", @@ -19216,6 +19235,16 @@ "license": "MIT", "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, + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4" + } + }, "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 1a179cd3c..6b138ed5b 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "husky": "^9.1.7", "ignore-loader": "^0.1.2", "jest": "^29.7.0", + "jest-canvas-mock": "^2.5.2", "jest-environment-jsdom": "^29.7.0", "less": "^4.2.1", "less-loader": "^12.2.0", diff --git a/src/components/EditFeatureDrawer/DeleteButton/index.spec.tsx b/src/components/EditFeatureDrawer/DeleteButton/index.spec.tsx index 0d5f57f61..0d5307ab7 100644 --- a/src/components/EditFeatureDrawer/DeleteButton/index.spec.tsx +++ b/src/components/EditFeatureDrawer/DeleteButton/index.spec.tsx @@ -6,7 +6,8 @@ import { fireEvent, render, screen, - waitFor + waitFor, + within } from '@testing-library/react'; import { @@ -19,8 +20,15 @@ import OlMap from 'ol/Map'; import OlSourceTileWMS from 'ol/source/TileWMS'; import OlView from 'ol/View'; +import { Provider } from 'react-redux'; + import { WmsLayer } from '@terrestris/ol-util/dist/typeUtils/typeUtils'; +import { renderInMapContext } from '@terrestris/react-util/dist/Util/rtlTestUtils'; + +import useExecuteWfsTransaction from '../../../hooks/useExecuteWfsTransaction'; +import useWriteWfsTransaction from '../../../hooks/useWriteWfsTransaction'; +import { store } from '../../../store/store'; import { createReduxWrapper } from '../../../utils/testUtils'; import DeleteButton from '.'; @@ -29,6 +37,9 @@ let mockLayer: WmsLayer; let mockFeature: Feature; let map: OlMap; +jest.mock('../../../hooks/useWriteWfsTransaction'); +jest.mock('../../../hooks/useExecuteWfsTransaction'); + describe('', () => { beforeEach(() => { @@ -96,14 +107,26 @@ describe('', () => { expect(deleteButton).toBeVisible(); }); - it('confirm popover is opened on click', async () => { - render( - , - { wrapper: createReduxWrapper() } + it('confirm popover is opened on click and delete is successfully executed on confirm', async () => { + const mockSuccessFunction = jest.fn(); + const mockTransaction = document.createElement('div'); + mockTransaction.textContent = 'Mocked Node'; + const mockWriteWfsTransaction = jest.fn().mockResolvedValue(mockTransaction); + const mockExecuteWfsTransaction = jest.fn().mockResolvedValue({}); + + (useWriteWfsTransaction as jest.Mock).mockReturnValueOnce(mockWriteWfsTransaction); + (useExecuteWfsTransaction as jest.Mock).mockReturnValueOnce(mockExecuteWfsTransaction); + + renderInMapContext( + map, + + + ); const deleteButton = screen.getByText('DeleteButton.title'); @@ -117,5 +140,52 @@ describe('', () => { await waitFor(() => { expect(popover).toBeVisible(); }); + + const tooltip = within(screen.getByRole('tooltip')); + + fireEvent.click(tooltip.getByText('OK')); + + await waitFor(() => { + expect(mockWriteWfsTransaction).toHaveBeenCalled(); + expect(mockExecuteWfsTransaction).toHaveBeenCalled(); + expect(mockSuccessFunction).toHaveBeenCalled(); + }); + }); + + it('Error function is called when delete operation can not be executed', async () => { + const mockErrorFunction = jest.fn(); + + renderInMapContext( + map, + + + + ); + + const deleteButton = screen.getByText('DeleteButton.title'); + expect(deleteButton).toBeVisible(); + + act(() => { + fireEvent.click(deleteButton); + }); + + const popover = document.querySelector('.ant-popover-open'); + await waitFor(() => { + expect(popover).toBeVisible(); + }); + + const tooltip = within(screen.getByRole('tooltip')); + + fireEvent.click(tooltip.getByText('OK')); + + await waitFor(() => { + expect(mockErrorFunction).toHaveBeenCalled(); + }); }); }); + diff --git a/src/components/EditFeatureDrawer/EditFeatureForm/index.spec.tsx b/src/components/EditFeatureDrawer/EditFeatureForm/index.spec.tsx index 432f6a95f..5a231114b 100644 --- a/src/components/EditFeatureDrawer/EditFeatureForm/index.spec.tsx +++ b/src/components/EditFeatureDrawer/EditFeatureForm/index.spec.tsx @@ -1,15 +1,54 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { + fireEvent, + render, + screen, + waitFor +} from '@testing-library/react'; import { Form } from 'antd'; +import { PropertyFormItemEditConfig } from '@terrestris/shogun-util/dist/model/Layer'; + +import { setFormDirty } from '../../../store/editFeature'; + import { createReduxWrapper } from '../../../utils/testUtils'; import EditFeatureForm, { EditFeatureFormProps } from '.'; +jest.mock('../../../hooks/useAppDispatch', () => () => jest.fn()); +jest.mock('../../../hooks/useAppSelector', () => jest.fn(() => false)); +jest.mock('../../../hooks/useSHOGunAPIClient', () => jest.fn(() => ({}))); +jest.mock('../../../store/editFeature', () => ({ + setFormDirty: jest.fn() +})); +jest.mock('../../DisplayField', () => { + const DisplayField = () =>
; + DisplayField.displayName = 'DisplayField'; + return DisplayField; +}); + +jest.mock('../../FileUpload', () => { + const FileUpload = () =>
; + FileUpload.displayName = 'FileUpload'; + return FileUpload; +}); + +jest.mock('../../ImageUpload', () => { + const ImageUpload = () =>
; + ImageUpload.displayName = 'ImageUpload'; + return ImageUpload; +}); + +jest.mock('../EditReferenceTable', () => { + const EditReferenceTable = () =>
; + EditReferenceTable.displayName = 'EditReferenceTable'; + return EditReferenceTable; +}); + describe('', () => { const EditFeatureFormWrapper = (props: Omit) => { const [form] = Form.useForm(); @@ -22,6 +61,37 @@ describe('', () => { ); }; + const formConfig: PropertyFormItemEditConfig[] = [ + { + propertyName: 'inputField', + displayName: 'Input Field', + component: 'INPUT', + required: true + }, + { + propertyName: 'selectField', + displayName: 'Select Field', + component: 'SELECT' + }, + { + propertyName: 'checkboxField', + displayName: 'Checkbox Field', + component: 'CHECKBOX' + }, + { + propertyName: 'uploadField', + displayName: 'Upload Field', + component: 'UPLOAD', + fieldProps: { type: 'FILE' } + }, + { + propertyName: 'imageUploadField', + displayName: 'Image Upload Field', + component: 'UPLOAD', + fieldProps: { type: 'IMAGE' } + } + ]; + it('is defined', () => { expect(EditFeatureForm).toBeDefined(); }); @@ -30,7 +100,9 @@ describe('', () => { const { container } = render( - , + , { wrapper: createReduxWrapper() }); @@ -38,5 +110,28 @@ describe('', () => { expect(container).toBeVisible(); const formElem = container.querySelector('.edit-feature-form'); expect(formElem).toBeVisible(); + + expect(screen.getByLabelText('Input Field')).toBeInTheDocument(); + expect(screen.getByLabelText('Select Field')).toBeInTheDocument(); + expect(screen.getByLabelText('Checkbox Field')).toBeInTheDocument(); + expect(screen.getByTestId('file-upload')).toBeInTheDocument(); + expect(screen.getByTestId('image-upload')).toBeInTheDocument(); + }); + + it('marks the form as dirty when values change', async () => { + render( + , + { + wrapper: createReduxWrapper() + }); + + const inputField = screen.getByLabelText('Input Field'); + await fireEvent.change(inputField, { target: { value: 'new value' } }); + + await waitFor(() => { + expect(setFormDirty).toHaveBeenCalledWith(true); + }); }); }); diff --git a/src/components/EditFeatureDrawer/EditFeatureGeometryToolbar/index.spec.tsx b/src/components/EditFeatureDrawer/EditFeatureGeometryToolbar/index.spec.tsx index 914239f0d..f6d1567fa 100644 --- a/src/components/EditFeatureDrawer/EditFeatureGeometryToolbar/index.spec.tsx +++ b/src/components/EditFeatureDrawer/EditFeatureGeometryToolbar/index.spec.tsx @@ -1,31 +1,239 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { + cleanup, + fireEvent, + render, + screen, + waitFor +} from '@testing-library/react'; +import { + Feature as FeatureGeoJson +} from 'geojson'; +import { Feature } from 'ol'; +import { MultiPolygon } from 'ol/geom'; +import OlGeometry from 'ol/geom/Geometry'; +import OlVectorLayer from 'ol/layer/Vector'; +import OlMap from 'ol/Map'; +import { fromLonLat } from 'ol/proj'; +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 useAppSelector from '../../../hooks/useAppSelector'; +import { EditLevel } from '../../../store/editFeature'; +import { store } from '../../../store/store'; import { createReduxWrapper } from '../../../utils/testUtils'; import EditFeatureGeometryToolbar from './'; +let map: OlMap; +let mockAllowedEditMode: EditLevel[] = ['EDIT_GEOMETRY']; + +jest.mock('../../../hooks/useAppSelector', () => ({ + __esModule: true, + default: jest.fn() +})); + +const feature: FeatureGeoJson = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [0, 0] + }, + id: 1, + properties: {} +}; + +const mockCoordinates = [ + [ + fromLonLat([-100, 40]), + fromLonLat([-90, 40]), + fromLonLat([-90, 30]), + fromLonLat([-100, 30]), + fromLonLat([-100, 40]) + ] +]; + +const mockFeature = { + geometry: new MultiPolygon([mockCoordinates]), + elementFormDefault: 'qualified', + targetNamespace: 'http://www.openplans.org/topp', + targetPrefix: 'topp', + featureTypes: [{ + typeName: 'states', + properties: [{ + name: 'the_geom', + nillable: true, + type: 'gml:MultiPolygon', + localType: 'MultiPolygon' + }] + }] +}; + +const mockVectorSource = new OlSourceVector({ + features: [new Feature(mockFeature)] +}); + +const mockVectorLayer = new OlVectorLayer({ + source: mockVectorSource +}); + +jest.mock('@terrestris/ol-util/dist/MapUtil/MapUtil'); + +const mockDescribeFeatureType = jest.fn().mockResolvedValue(mockFeature); +jest.mock('../../../hooks/useExecuteWfsDescribeFeatureType', () => { + const originalModule = jest.requireActual('../../../hooks/useExecuteWfsDescribeFeatureType'); + return { + __esModule: true, + ...originalModule, + default: jest.fn(() => mockDescribeFeatureType) + }; +}); + describe('EditFeatureGeometryToolbar', () => { + beforeEach(() => { + document.body.innerHTML = '
'; + + map = new OlMap({ + target: 'map', + view: new OlView({ + zoom: 10 + }), + controls: [], + layers: [] + }); + }); + + afterEach(() => { + cleanup(); + }); + it('is defined', () => { expect(EditFeatureGeometryToolbar).toBeDefined(); }); - it('can be rendered', () => { + it('can be rendered', async () => { + const getDigitizeLayerMock = jest.spyOn(DigitizeUtil, 'getDigitizeLayer').mockReturnValue(mockVectorLayer); + jest.requireMock('../../../hooks/useAppSelector').default.mockImplementation((callback: any) => callback({ + editFeature: { + userEditMode: mockAllowedEditMode + } + })); + const { container } = renderInMapContext( + map, + + , + + ); + + expect(container).toBeVisible(); + + await waitFor(() => { + expect(useAppSelector).toHaveBeenCalled(); + }); + + expect(screen.queryByText('EditFeatureGeometryToolbar.draw')).toBeNull(); + expect(screen.queryByText('EditFeatureGeometryToolbar.edit')).toBeNull(); + expect(screen.queryByText('EditFeatureGeometryToolbar.delete')).toBeNull(); + + await waitFor(() => { + expect(screen.getByRole('toolbar')).toBeInTheDocument(); + }); + + getDigitizeLayerMock.mockRestore(); + }); + + it('is empty when not allowed to edit geometery', async () => { + mockAllowedEditMode = []; + jest.requireMock('../../../hooks/useAppSelector').default.mockImplementation((callback: any) => callback({ + editFeature: { + userEditMode: mockAllowedEditMode + } + })); const { container } = render( , { wrapper: createReduxWrapper() }); expect(container).toBeVisible(); + + await waitFor(() => { + expect(useAppSelector).toHaveBeenCalled(); + }); + expect(screen.queryByRole('toolbar')).toBeNull(); + }); + + it('renders draw button in edit model create', async () => { + mockAllowedEditMode = ['EDIT_GEOMETRY', 'CREATE']; + jest.requireMock('../../../hooks/useAppSelector').default.mockImplementation((callback: any) => callback({ + editFeature: { + userEditMode: mockAllowedEditMode + } + })); + const { container } = renderInMapContext( + map, + + , + + ); + + expect(container).toBeVisible(); + + await waitFor(() => { + expect(useAppSelector).toHaveBeenCalled(); + }); + + const drawButton = container.querySelector('button[name="draw"]'); + expect(drawButton).toBeInTheDocument(); + + await waitFor(() => { + fireEvent.click(drawButton); + }); + + expect(drawButton).toHaveAttribute('aria-pressed', 'true'); + }); + + it('renders delete button', async () => { + mockAllowedEditMode = ['EDIT_GEOMETRY', 'DELETE']; + jest.requireMock('../../../hooks/useAppSelector').default.mockImplementation((callback: any) => callback({ + editFeature: { + userEditMode: mockAllowedEditMode + } + })); + const { container } = renderInMapContext( + map, + + , + + ); + + expect(container).toBeVisible(); + + await waitFor(() => { + expect(useAppSelector).toHaveBeenCalled(); + }); + + const deleteButton = container.querySelector('button[name="delete"]'); + expect(deleteButton).toBeInTheDocument(); + + await waitFor(() => { + fireEvent.click(deleteButton); + }); + + expect(deleteButton).toHaveAttribute('aria-pressed', 'true'); }); });