diff --git a/README.md b/README.md index 2b2e56d28..aab023092 100644 --- a/README.md +++ b/README.md @@ -160,3 +160,21 @@ Once all of the dependencies are installed, you'll need to do the following to e - See the `settings.py` value `JWT_EXCHANGE_PROVIDERS`. You will need to set the environment variable `GOOGLE_MOBILE_CLIENT_ID` 5. Set up mobile config to connect to backend - See [Android emulator networking](https://developer.android.com/studio/run/emulator-networking.html) for details on how to connect to your backend instance + +# Testing + +We use Jest for testing. Project tests are partitioned into **unit** (fast, no external dependencies), and **integration** (slow, may rely on disk access or network calls etc). You can run unit tests with `npm run test:unit` and integration tests with `npm run test:integration`; run all tests with `npm run test`. Tests are run automatically on GitHub and must pass for a PR to be eligible for merging. + +### Configuration + +Unit and integration tests have their own Jest configuration files to reflect their needs. Unit tests are covered by the `jest.unit.config.js`, while integration tests are covered by `jest.integration.config.js`. The `jest` section of `package.json` lists these both as projects for Jest to run, which is how `npm run test` runs them all. + +Unit tests are written inline in `src` and should live parallel to the source files they cover. Unit tests must be suffixed with `.test.ts/tsx/js/jsx` for the test runner to recognize them. + +Integration tests exist separately in `__tests__`. Any file containing test definitions in this directory will be picked up by the integration test runner. + +Additionally, both suites of tests have access to the `jest` directory, which contains utility and common setup code. This directory is mapped to the `@testing` import path via `jest` configuration. + +### Snapshot tests + +A subset of our integration tests are **snapshot tests**, which are organized under `__tests/snapshot`. These run application screens in a stubbed-out test harness and compare the resulting React Native DOM tree against one stored in a corresponding snapshot file, highlighting any differences. If a snapshot needs to be updated as a result of a change, you can run `npm run test -- -u` to automatically update them from the results of a test run. \ No newline at end of file diff --git a/dev-client/__tests__/integration/CreateProjectScreen-test.tsx b/dev-client/__tests__/snapshot/CreateProjectScreen-test.tsx similarity index 90% rename from dev-client/__tests__/integration/CreateProjectScreen-test.tsx rename to dev-client/__tests__/snapshot/CreateProjectScreen-test.tsx index fea4f2b5a..c23629a35 100644 --- a/dev-client/__tests__/integration/CreateProjectScreen-test.tsx +++ b/dev-client/__tests__/snapshot/CreateProjectScreen-test.tsx @@ -14,8 +14,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ -import {testState} from '@testing/data'; -import {render} from '@testing/utils'; +import {testState} from '@testing/integration/data'; +import {render} from '@testing/integration/utils'; import {CreateProjectScreen} from 'terraso-mobile-client/screens/CreateProjectScreen/CreateProjectScreen'; diff --git a/dev-client/__tests__/integration/CreateSiteScreen-test.tsx b/dev-client/__tests__/snapshot/CreateSiteScreen-test.tsx similarity index 95% rename from dev-client/__tests__/integration/CreateSiteScreen-test.tsx rename to dev-client/__tests__/snapshot/CreateSiteScreen-test.tsx index aa216eb46..722aacc2f 100644 --- a/dev-client/__tests__/integration/CreateSiteScreen-test.tsx +++ b/dev-client/__tests__/snapshot/CreateSiteScreen-test.tsx @@ -15,7 +15,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import {render} from '@testing/utils'; +import {render} from '@testing/integration/utils'; import {CreateSiteScreen} from 'terraso-mobile-client/screens/CreateSiteScreen/CreateSiteScreen'; diff --git a/dev-client/__tests__/integration/LocationDashboardScreen-test.tsx b/dev-client/__tests__/snapshot/LocationDashboardScreen-test.tsx similarity index 90% rename from dev-client/__tests__/integration/LocationDashboardScreen-test.tsx rename to dev-client/__tests__/snapshot/LocationDashboardScreen-test.tsx index 82c2fd543..11538c060 100644 --- a/dev-client/__tests__/integration/LocationDashboardScreen-test.tsx +++ b/dev-client/__tests__/snapshot/LocationDashboardScreen-test.tsx @@ -14,8 +14,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ -import {testState} from '@testing/data'; -import {render} from '@testing/utils'; +import {testState} from '@testing/integration/data'; +import {render} from '@testing/integration/utils'; import {LocationDashboardScreen} from 'terraso-mobile-client/screens/LocationScreens/LocationDashboardScreen'; diff --git a/dev-client/__tests__/integration/LoginScreen-test.tsx b/dev-client/__tests__/snapshot/LoginScreen-test.tsx similarity index 95% rename from dev-client/__tests__/integration/LoginScreen-test.tsx rename to dev-client/__tests__/snapshot/LoginScreen-test.tsx index 166205a6f..f18c49236 100644 --- a/dev-client/__tests__/integration/LoginScreen-test.tsx +++ b/dev-client/__tests__/snapshot/LoginScreen-test.tsx @@ -15,7 +15,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import {render} from '@testing/utils'; +import {render} from '@testing/integration/utils'; import {LoginScreen} from 'terraso-mobile-client/screens/LoginScreen'; diff --git a/dev-client/__tests__/integration/ProjectViewScreen-test.tsx b/dev-client/__tests__/snapshot/ProjectViewScreen-test.tsx similarity index 90% rename from dev-client/__tests__/integration/ProjectViewScreen-test.tsx rename to dev-client/__tests__/snapshot/ProjectViewScreen-test.tsx index 2ffcacf54..e072b4b7d 100644 --- a/dev-client/__tests__/integration/ProjectViewScreen-test.tsx +++ b/dev-client/__tests__/snapshot/ProjectViewScreen-test.tsx @@ -14,8 +14,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ -import {testState} from '@testing/data'; -import {render} from '@testing/utils'; +import {testState} from '@testing/integration/data'; +import {render} from '@testing/integration/utils'; import {ProjectViewScreen} from 'terraso-mobile-client/screens/ProjectViewScreen'; diff --git a/dev-client/__tests__/integration/SlopeScreen-test.tsx b/dev-client/__tests__/snapshot/SlopeScreen-test.tsx similarity index 94% rename from dev-client/__tests__/integration/SlopeScreen-test.tsx rename to dev-client/__tests__/snapshot/SlopeScreen-test.tsx index e15ac61b2..6c98466de 100644 --- a/dev-client/__tests__/integration/SlopeScreen-test.tsx +++ b/dev-client/__tests__/snapshot/SlopeScreen-test.tsx @@ -15,8 +15,8 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import {testState} from '@testing/data'; -import {render} from '@testing/utils'; +import {testState} from '@testing/integration/data'; +import {render} from '@testing/integration/utils'; import {methodRequired} from 'terraso-client-shared/soilId/soilIdSlice'; import {collectionMethods} from 'terraso-client-shared/soilId/soilIdTypes'; diff --git a/dev-client/__tests__/integration/SoilScreen-test.tsx b/dev-client/__tests__/snapshot/SoilScreen-test.tsx similarity index 90% rename from dev-client/__tests__/integration/SoilScreen-test.tsx rename to dev-client/__tests__/snapshot/SoilScreen-test.tsx index dc2309b2a..77e2cad7c 100644 --- a/dev-client/__tests__/integration/SoilScreen-test.tsx +++ b/dev-client/__tests__/snapshot/SoilScreen-test.tsx @@ -14,8 +14,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ -import {testState} from '@testing/data'; -import {render} from '@testing/utils'; +import {testState} from '@testing/integration/data'; +import {render} from '@testing/integration/utils'; import {SoilScreen} from 'terraso-mobile-client/screens/SoilScreen/SoilScreen'; diff --git a/dev-client/__tests__/integration/__snapshots__/CreateProjectScreen-test.tsx.snap b/dev-client/__tests__/snapshot/__snapshots__/CreateProjectScreen-test.tsx.snap similarity index 100% rename from dev-client/__tests__/integration/__snapshots__/CreateProjectScreen-test.tsx.snap rename to dev-client/__tests__/snapshot/__snapshots__/CreateProjectScreen-test.tsx.snap diff --git a/dev-client/__tests__/integration/__snapshots__/CreateSiteScreen-test.tsx.snap b/dev-client/__tests__/snapshot/__snapshots__/CreateSiteScreen-test.tsx.snap similarity index 100% rename from dev-client/__tests__/integration/__snapshots__/CreateSiteScreen-test.tsx.snap rename to dev-client/__tests__/snapshot/__snapshots__/CreateSiteScreen-test.tsx.snap diff --git a/dev-client/__tests__/integration/__snapshots__/LocationDashboardScreen-test.tsx.snap b/dev-client/__tests__/snapshot/__snapshots__/LocationDashboardScreen-test.tsx.snap similarity index 100% rename from dev-client/__tests__/integration/__snapshots__/LocationDashboardScreen-test.tsx.snap rename to dev-client/__tests__/snapshot/__snapshots__/LocationDashboardScreen-test.tsx.snap diff --git a/dev-client/__tests__/integration/__snapshots__/LoginScreen-test.tsx.snap b/dev-client/__tests__/snapshot/__snapshots__/LoginScreen-test.tsx.snap similarity index 100% rename from dev-client/__tests__/integration/__snapshots__/LoginScreen-test.tsx.snap rename to dev-client/__tests__/snapshot/__snapshots__/LoginScreen-test.tsx.snap diff --git a/dev-client/__tests__/integration/__snapshots__/ProjectViewScreen-test.tsx.snap b/dev-client/__tests__/snapshot/__snapshots__/ProjectViewScreen-test.tsx.snap similarity index 100% rename from dev-client/__tests__/integration/__snapshots__/ProjectViewScreen-test.tsx.snap rename to dev-client/__tests__/snapshot/__snapshots__/ProjectViewScreen-test.tsx.snap diff --git a/dev-client/__tests__/integration/__snapshots__/SlopeScreen-test.tsx.snap b/dev-client/__tests__/snapshot/__snapshots__/SlopeScreen-test.tsx.snap similarity index 100% rename from dev-client/__tests__/integration/__snapshots__/SlopeScreen-test.tsx.snap rename to dev-client/__tests__/snapshot/__snapshots__/SlopeScreen-test.tsx.snap diff --git a/dev-client/__tests__/integration/__snapshots__/SoilScreen-test.tsx.snap b/dev-client/__tests__/snapshot/__snapshots__/SoilScreen-test.tsx.snap similarity index 100% rename from dev-client/__tests__/integration/__snapshots__/SoilScreen-test.tsx.snap rename to dev-client/__tests__/snapshot/__snapshots__/SoilScreen-test.tsx.snap diff --git a/dev-client/jest.integration.config.js b/dev-client/jest.integration.config.js new file mode 100644 index 000000000..4922ff0d9 --- /dev/null +++ b/dev-client/jest.integration.config.js @@ -0,0 +1,37 @@ +/* + * Copyright © 2024 Technology Matters + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +module.exports = { + displayName: 'integration', + testMatch: ['**/__tests__/**/*[.-]test.[jt]s?(x)'], + preset: 'jest-expo', + setupFilesAfterEnv: [ + '@testing-library/jest-native/extend-expect', + '@rnmapbox/maps/setup-jest', + './node_modules/react-native-mmkv-storage/jest/mmkvJestSetup.js', + '/jest/integration/setup.ts', + ], + transformIgnorePatterns: [ + 'node_modules/(?!(react-native|@react-native|react-native-cookies|uuid|react-native-mmkv-storage|react-native-autocomplete-input|expo(nent)?|@expo(nent)?/.*)|expo-constants|@rnmapbox/)', + ], + moduleNameMapper: { + '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/__mocks__/fileMock.ts', + '^@testing/(.*)': '/jest/$1', + '^terraso-mobile-client/(.*)': '/src/$1', + }, +}; diff --git a/dev-client/jest.unit.config.js b/dev-client/jest.unit.config.js new file mode 100644 index 000000000..3e3471fcc --- /dev/null +++ b/dev-client/jest.unit.config.js @@ -0,0 +1,31 @@ +/* + * Copyright © 2024 Technology Matters + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +module.exports = { + displayName: 'unit', + testMatch: ['**/src/**/*.test.[jt]s?(x)'], + preset: 'ts-jest', + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['/jest/unit/setup.ts'], + clearMocks: true, + moduleNameMapper: { + '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/__mocks__/fileMock.ts', + '^@testing/(.*)': '/jest/$1', + '^terraso-mobile-client/(.*)': '/src/$1', + }, +}; diff --git a/dev-client/jest/data.ts b/dev-client/jest/integration/data.ts similarity index 100% rename from dev-client/jest/data.ts rename to dev-client/jest/integration/data.ts diff --git a/dev-client/jest/setup.ts b/dev-client/jest/integration/setup.ts similarity index 100% rename from dev-client/jest/setup.ts rename to dev-client/jest/integration/setup.ts diff --git a/dev-client/jest/utils.tsx b/dev-client/jest/integration/utils.tsx similarity index 100% rename from dev-client/jest/utils.tsx rename to dev-client/jest/integration/utils.tsx diff --git a/dev-client/jest/unit/setup.ts b/dev-client/jest/unit/setup.ts new file mode 100644 index 000000000..0fb628e87 --- /dev/null +++ b/dev-client/jest/unit/setup.ts @@ -0,0 +1,18 @@ +/* + * Copyright © 2024 Technology Matters + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +// fill me in if you need special config for unit testing diff --git a/dev-client/package-lock.json b/dev-client/package-lock.json index d1d9e9922..62bbe217c 100644 --- a/dev-client/package-lock.json +++ b/dev-client/package-lock.json @@ -92,6 +92,7 @@ "react-devtools": "^5.3.1", "react-native-svg-transformer": "^1.5.0", "react-test-renderer": "18.2.0", + "ts-jest": "^29.2.3", "ts-morph": "^23.0.0", "ts-node": "^10.9.2", "typescript": "^5.5.3" @@ -10859,6 +10860,12 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -11506,6 +11513,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -13350,6 +13369,21 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron": { "version": "23.3.13", "resolved": "https://registry.npmjs.org/electron/-/electron-23.3.13.tgz", @@ -15340,6 +15374,27 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -17408,6 +17463,116 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/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/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -20100,6 +20265,12 @@ "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -25644,6 +25815,75 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/ts-jest": { + "version": "29.2.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.3.tgz", + "integrity": "sha512-yCcfVdiBFngVz9/keHin9EnsrQtQtEu3nRykNy9RVp+FiPFFbPJ3Sg6Qg4+TkmH0vMP5qsTKgXSsk80HRwvdgQ==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/ts-morph": { "version": "23.0.0", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-23.0.0.tgz", diff --git a/dev-client/package.json b/dev-client/package.json index a1bd687e3..51fb5b0c5 100644 --- a/dev-client/package.json +++ b/dev-client/package.json @@ -11,6 +11,8 @@ "format-js": "npm run format", "start": "expo start --dev-client", "test": "jest", + "test:unit": "jest -c jest.unit.config.js", + "test:integration": "jest -c jest.integration.config.js", "update-tests": "npm run test -- -u", "check-ts": "tsc --noEmit", "check-modules": "depcheck", @@ -101,25 +103,15 @@ "react-devtools": "^5.3.1", "react-native-svg-transformer": "^1.5.0", "react-test-renderer": "18.2.0", + "ts-jest": "^29.2.3", "ts-morph": "^23.0.0", "ts-node": "^10.9.2", "typescript": "^5.5.3" }, "jest": { - "preset": "jest-expo", - "setupFilesAfterEnv": [ - "@testing-library/jest-native/extend-expect", - "@rnmapbox/maps/setup-jest", - "./node_modules/react-native-mmkv-storage/jest/mmkvJestSetup.js", - "/jest/setup.ts" - ], - "moduleNameMapper": { - "\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.ts", - "^@testing/(.*)": "/jest/$1", - "^terraso-mobile-client/(.*)": "/src/$1" - }, - "transformIgnorePatterns": [ - "node_modules/(?!(react-native|@react-native|react-native-cookies|uuid|react-native-mmkv-storage|react-native-autocomplete-input|expo(nent)?|@expo(nent)?/.*)|expo-constants|@rnmapbox/)" + "projects": [ + "/jest.unit.config.js", + "/jest.integration.config.js" ] }, "expo": { @@ -130,6 +122,13 @@ ] } }, + "ignore": [ + "**/*.test.ts", + "**/*.test.tsx", + "**/__tests__/**", + "src/tests", + "jest" + ], "engines": { "node": ">=18" } diff --git a/dev-client/src/components/tables/soilProperties/SoilPropertiesData.tsx b/dev-client/src/components/tables/soilProperties/SoilPropertiesData.tsx index 7a9c48a80..bcab3b3ab 100644 --- a/dev-client/src/components/tables/soilProperties/SoilPropertiesData.tsx +++ b/dev-client/src/components/tables/soilProperties/SoilPropertiesData.tsx @@ -32,7 +32,7 @@ import { import { fullMunsellColor, munsellToString, -} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/munsellConversions'; +} from 'terraso-mobile-client/model/color/munsellConversions'; export type SoilPropertiesDataTableRow = { depth: DepthInterval; diff --git a/dev-client/src/model/color/munsellConversions.test.tsx b/dev-client/src/model/color/munsellConversions.test.tsx new file mode 100644 index 000000000..02d3659d0 --- /dev/null +++ b/dev-client/src/model/color/munsellConversions.test.tsx @@ -0,0 +1,139 @@ +/* + * Copyright © 2024 Technology Matters + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { + fullMunsellColor, + isColorComplete, + LABToMunsell, + munsellHVCToLAB, + munsellToRGB, +} from 'terraso-mobile-client/model/color/munsellConversions'; + +describe('isColorComplete', () => { + test('returns true if complete', () => { + expect( + isColorComplete({ + colorHue: 1, + colorValue: 2, + colorChroma: 3, + }), + ).toEqual(true); + }); + + test('returns false if value is missing any number', () => { + expect(isColorComplete({})).toEqual(false); + + expect( + isColorComplete({ + colorHue: 1, + colorValue: null, + colorChroma: null, + }), + ).toEqual(false); + + expect( + isColorComplete({ + colorHue: null, + colorValue: 1, + colorChroma: null, + }), + ).toEqual(false); + + expect( + isColorComplete({ + colorHue: null, + colorValue: null, + colorChroma: 1, + }), + ).toEqual(false); + }); +}); + +describe('fullMunsellColor', () => { + test('returns the color if complete', () => { + expect( + fullMunsellColor({ + colorHue: 1, + colorValue: 2, + colorChroma: 3, + }), + ).toEqual({ + colorHue: 1, + colorValue: 2, + colorChroma: 3, + }); + }); + + test('returns undefined if value is missing any number', () => { + expect(fullMunsellColor({})).toBeUndefined(); + + expect( + fullMunsellColor({ + colorHue: 1, + colorValue: null, + colorChroma: null, + }), + ).toBeUndefined(); + + expect( + fullMunsellColor({ + colorHue: null, + colorValue: 1, + colorChroma: null, + }), + ).toBeUndefined(); + + expect( + fullMunsellColor({ + colorHue: null, + colorValue: null, + colorChroma: 1, + }), + ).toBeUndefined(); + }); +}); + +describe('munsellToRGB', () => { + it('does the magic math', () => { + const result = munsellToRGB({ + colorHue: 0.5, + colorValue: 0.5, + colorChroma: 0.5, + }); + expect(result[0]).toBeCloseTo(22); + expect(result[1]).toBeCloseTo(15); + expect(result[2]).toBeCloseTo(19); + }); +}); + +describe('munsellHVCToLAB', () => { + it('does the magic math', () => { + const result = munsellHVCToLAB([0.5, 0.5, 0.5]); + expect(result.L).toBeCloseTo(5.123); + expect(result.A).toBeCloseTo(3.437); + expect(result.B).toBeCloseTo(-0.784); + }); +}); + +describe('LABToMunsell', () => { + it('does the magic math', () => { + const result = LABToMunsell({L: 5.123, A: 3.437, B: -0.784}); + expect(result.colorHue).toBeCloseTo(0.5); + expect(result.colorValue).toBeCloseTo(0.5); + expect(result.colorChroma).toBeCloseTo(0.5); + }); +}); diff --git a/dev-client/src/screens/SoilScreen/ColorScreen/utils/munsellConversions.ts b/dev-client/src/model/color/munsellConversions.ts similarity index 95% rename from dev-client/src/screens/SoilScreen/ColorScreen/utils/munsellConversions.ts rename to dev-client/src/model/color/munsellConversions.ts index 91bd4e04c..2fec0100b 100644 --- a/dev-client/src/screens/SoilScreen/ColorScreen/utils/munsellConversions.ts +++ b/dev-client/src/model/color/munsellConversions.ts @@ -53,7 +53,7 @@ import { } from 'terraso-client-shared/soilId/soilIdTypes'; import {entries} from 'terraso-client-shared/utils'; -import {SOIL_COLORS} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/soilColors'; +import {SOIL_COLORS} from 'terraso-mobile-client/model/color/soilColors'; const SOIL_COLOR_SIMILARITY_THRESHOLD = 5; @@ -81,14 +81,20 @@ export type PartialMunsellColor = { const COLOR_COUNT = 16; +export const isColorComplete = ( + soilData: T & PartialMunsellColor, +): soilData is T & MunsellColor => { + return ( + typeof soilData?.colorHue === 'number' && + typeof soilData.colorValue === 'number' && + typeof soilData.colorChroma === 'number' + ); +}; + export const fullMunsellColor = ( color: PartialMunsellColor, ): MunsellColor | undefined => { - if ( - typeof color.colorHue === 'number' && - typeof color.colorChroma === 'number' && - typeof color.colorValue === 'number' - ) { + if (isColorComplete(color)) { return { colorHue: color.colorHue, colorChroma: color.colorChroma, diff --git a/dev-client/src/screens/SoilScreen/ColorScreen/utils/soilColorValidation.ts b/dev-client/src/model/color/soilColorValidation.ts similarity index 90% rename from dev-client/src/screens/SoilScreen/ColorScreen/utils/soilColorValidation.ts rename to dev-client/src/model/color/soilColorValidation.ts index a0b4e41ab..2c6bfcc71 100644 --- a/dev-client/src/screens/SoilScreen/ColorScreen/utils/soilColorValidation.ts +++ b/dev-client/src/model/color/soilColorValidation.ts @@ -30,12 +30,10 @@ import { colorHueSubsteps, ColorValue, colorValues, - DepthDependentSoilData, SoilColorHue, } from 'terraso-client-shared/soilId/soilIdTypes'; -import {MunsellColor} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/munsellConversions'; -import {SOIL_COLORS} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/soilColors'; +import {SOIL_COLORS} from 'terraso-mobile-client/model/color/soilColors'; export type ColorProperties = { hue: SoilColorHue | null; @@ -178,13 +176,3 @@ export const isChromaValid = (color: ColorProperties) => { validProperties(color).chromas.includes(color.chroma) ); }; - -export const isColorComplete = ( - soilData: DepthDependentSoilData, -): soilData is DepthDependentSoilData & MunsellColor => { - return ( - typeof soilData?.colorHue === 'number' && - typeof soilData.colorValue === 'number' && - typeof soilData.colorChroma === 'number' - ); -}; diff --git a/dev-client/src/screens/SoilScreen/ColorScreen/utils/soilColors.ts b/dev-client/src/model/color/soilColors.ts similarity index 100% rename from dev-client/src/screens/SoilScreen/ColorScreen/utils/soilColors.ts rename to dev-client/src/model/color/soilColors.ts diff --git a/dev-client/src/screens/ColorAnalysisScreen/ColorAnalysisHomeScreen.tsx b/dev-client/src/screens/ColorAnalysisScreen/ColorAnalysisHomeScreen.tsx index 879dbe521..2831b62da 100644 --- a/dev-client/src/screens/ColorAnalysisScreen/ColorAnalysisHomeScreen.tsx +++ b/dev-client/src/screens/ColorAnalysisScreen/ColorAnalysisHomeScreen.tsx @@ -40,19 +40,19 @@ import { Row, Text, } from 'terraso-mobile-client/components/NativeBaseAdapters'; -import {useNavigation} from 'terraso-mobile-client/navigation/hooks/useNavigation'; -import {useColorAnalysisContext} from 'terraso-mobile-client/screens/ColorAnalysisScreen/context/colorAnalysisContext'; -import {useColorAnalysisNavigation} from 'terraso-mobile-client/screens/ColorAnalysisScreen/navigation/navigation'; -import {ScreenScaffold} from 'terraso-mobile-client/screens/ScreenScaffold'; -import {ColorDisplay} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/components/ColorDisplay'; -import {PhotoConditions} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/components/PhotoConditions'; import { getColor, InvalidColorResult, MunsellColor, REFERENCES, RGBA, -} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/munsellConversions'; +} from 'terraso-mobile-client/model/color/munsellConversions'; +import {useNavigation} from 'terraso-mobile-client/navigation/hooks/useNavigation'; +import {useColorAnalysisContext} from 'terraso-mobile-client/screens/ColorAnalysisScreen/context/colorAnalysisContext'; +import {useColorAnalysisNavigation} from 'terraso-mobile-client/screens/ColorAnalysisScreen/navigation/navigation'; +import {ScreenScaffold} from 'terraso-mobile-client/screens/ScreenScaffold'; +import {ColorDisplay} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/components/ColorDisplay'; +import {PhotoConditions} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/components/PhotoConditions'; import {useDispatch} from 'terraso-mobile-client/store'; const analyzeImage = async ({ diff --git a/dev-client/src/screens/SoilScreen/ColorScreen/ColorScreen.tsx b/dev-client/src/screens/SoilScreen/ColorScreen/ColorScreen.tsx index 4db120ffb..874e03652 100644 --- a/dev-client/src/screens/SoilScreen/ColorScreen/ColorScreen.tsx +++ b/dev-client/src/screens/SoilScreen/ColorScreen/ColorScreen.tsx @@ -32,13 +32,15 @@ import { Text, } from 'terraso-mobile-client/components/NativeBaseAdapters'; import {InfoOverlaySheetButton} from 'terraso-mobile-client/components/sheets/InfoOverlaySheetButton'; +import { + isColorComplete, + MunsellColor, +} from 'terraso-mobile-client/model/color/munsellConversions'; import {CameraWorkflow} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/components/CameraWorkflow'; import {ColorDisplay} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/components/ColorDisplay'; import {ManualWorkflow} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/components/ManualWorkflow'; import {PhotoConditions} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/components/PhotoConditions'; import {SwitchWorkflowButton} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/components/SwitchWorkflowButton'; -import {MunsellColor} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/munsellConversions'; -import {isColorComplete} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/soilColorValidation'; import { SoilPitInputScreenProps, SoilPitInputScreenScaffold, diff --git a/dev-client/src/screens/SoilScreen/ColorScreen/components/ColorDisplay.tsx b/dev-client/src/screens/SoilScreen/ColorScreen/components/ColorDisplay.tsx index 3b0a3434c..746e3a8b2 100644 --- a/dev-client/src/screens/SoilScreen/ColorScreen/components/ColorDisplay.tsx +++ b/dev-client/src/screens/SoilScreen/ColorScreen/components/ColorDisplay.tsx @@ -28,7 +28,7 @@ import { MunsellColor, munsellToRGB, munsellToString, -} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/munsellConversions'; +} from 'terraso-mobile-client/model/color/munsellConversions'; type Props = { color: MunsellColor; diff --git a/dev-client/src/screens/SoilScreen/ColorScreen/components/ManualWorkflow.tsx b/dev-client/src/screens/SoilScreen/ColorScreen/components/ManualWorkflow.tsx index 6697bba4f..e5f3c7ca6 100644 --- a/dev-client/src/screens/SoilScreen/ColorScreen/components/ManualWorkflow.tsx +++ b/dev-client/src/screens/SoilScreen/ColorScreen/components/ManualWorkflow.tsx @@ -39,18 +39,18 @@ import { Paragraph, Row, } from 'terraso-mobile-client/components/NativeBaseAdapters'; -import {SwitchWorkflowButton} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/components/SwitchWorkflowButton'; import { + isColorComplete, parseMunsellHue, renderMunsellHue, -} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/munsellConversions'; +} from 'terraso-mobile-client/model/color/munsellConversions'; import { ColorProperties, ColorPropertyUpdate, - isColorComplete, updateColorSelections, validProperties, -} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/soilColorValidation'; +} from 'terraso-mobile-client/model/color/soilColorValidation'; +import {SwitchWorkflowButton} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/components/SwitchWorkflowButton'; import {SoilPitInputScreenProps} from 'terraso-mobile-client/screens/SoilScreen/components/SoilPitInputScreenScaffold'; import {useDispatch, useSelector} from 'terraso-mobile-client/store'; diff --git a/dev-client/src/screens/SoilScreen/ColorScreen/components/SwitchWorkflowButton.tsx b/dev-client/src/screens/SoilScreen/ColorScreen/components/SwitchWorkflowButton.tsx index 78e950f85..899dcf17d 100644 --- a/dev-client/src/screens/SoilScreen/ColorScreen/components/SwitchWorkflowButton.tsx +++ b/dev-client/src/screens/SoilScreen/ColorScreen/components/SwitchWorkflowButton.tsx @@ -24,8 +24,8 @@ import {selectDepthDependentData} from 'terraso-client-shared/selectors'; import {updateDepthDependentSoilData} from 'terraso-client-shared/soilId/soilIdSlice'; import {ConfirmModal} from 'terraso-mobile-client/components/modals/ConfirmModal'; +import {isColorComplete} from 'terraso-mobile-client/model/color/munsellConversions'; import {updatePreferences} from 'terraso-mobile-client/model/preferences/preferencesSlice'; -import {isColorComplete} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/soilColorValidation'; import {SoilPitInputScreenProps} from 'terraso-mobile-client/screens/SoilScreen/components/SoilPitInputScreenScaffold'; import {useDispatch, useSelector} from 'terraso-mobile-client/store'; diff --git a/dev-client/src/screens/SoilScreen/components/RenderValues.tsx b/dev-client/src/screens/SoilScreen/components/RenderValues.tsx index 5520009a7..7f23c6bf7 100644 --- a/dev-client/src/screens/SoilScreen/components/RenderValues.tsx +++ b/dev-client/src/screens/SoilScreen/components/RenderValues.tsx @@ -24,9 +24,11 @@ import { } from 'terraso-client-shared/soilId/soilIdSlice'; import {Row, Text} from 'terraso-mobile-client/components/NativeBaseAdapters'; +import { + isColorComplete, + munsellToString, +} from 'terraso-mobile-client/model/color/munsellConversions'; import {ColorDisplay} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/components/ColorDisplay'; -import {munsellToString} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/munsellConversions'; -import {isColorComplete} from 'terraso-mobile-client/screens/SoilScreen/ColorScreen/utils/soilColorValidation'; export const renderDepth = ( t: TFunction,