From a54b3198f23be191ff89eb288bd92f524591258f Mon Sep 17 00:00:00 2001 From: Nathan Franklin Date: Mon, 11 Nov 2024 21:07:12 +0100 Subject: [PATCH] Task/WG-368 improve unit testing of requests (#280) * Update global test setup to check to make sure requests are mocked. * also ignore some warnings. * Add nock for request mocking * Update imports * Fix linting issues * Set up nock so we handle CORS * Add tapisUrl * Use nock in testing * Switch from nock to msw * Remove empty file * Add missing fixture * Ingnore warnings * Refactor into test dir * Use test render util * Update comment * Add missing handlers.ts --- react/jest.config.cjs | 13 +- react/jest.setup.ts | 86 ++++ react/package-lock.json | 423 ++++++++++++++++-- react/package.json | 3 + react/src/AppRouter.tsx | 4 +- .../__fixtures__/appConfigurationFixture.ts | 18 +- react/src/__fixtures__/authStateFixtures.ts | 2 +- react/src/__fixtures__/systemsFixture.ts | 47 ++ .../CreateMapModal/CreateMapModal.test.tsx | 65 ++- .../environment/getLocalAppConfiguration.ts | 35 ++ .../hooks/environment/useAppConfiguration.ts | 70 +-- react/src/hooks/environment/utils.ts | 43 ++ react/src/pages/Callback/Callback.test.tsx | 14 +- react/src/pages/Login/Login.test.tsx | 18 +- react/src/pages/MainMenu/MainMenu.test.tsx | 35 +- react/src/redux/authSlice.ts | 2 +- react/src/requests.test.ts | 2 +- react/src/requests.ts | 11 +- react/src/test/handlers.ts | 48 ++ react/src/test/testUtil.tsx | 33 ++ react/src/testUtil.ts | 5 - react/src/types/environment.ts | 6 +- react/src/types/projects.ts | 2 +- react/src/utils/authUtils.test.ts | 2 +- react/src/utils/authUtils.ts | 2 +- react/tsconfig.json | 2 +- 26 files changed, 782 insertions(+), 209 deletions(-) create mode 100644 react/jest.setup.ts create mode 100644 react/src/__fixtures__/systemsFixture.ts create mode 100644 react/src/hooks/environment/getLocalAppConfiguration.ts create mode 100644 react/src/hooks/environment/utils.ts create mode 100644 react/src/test/handlers.ts create mode 100644 react/src/test/testUtil.tsx delete mode 100644 react/src/testUtil.ts diff --git a/react/jest.config.cjs b/react/jest.config.cjs index d63ce4ec..f95e0349 100644 --- a/react/jest.config.cjs +++ b/react/jest.config.cjs @@ -7,6 +7,7 @@ const esModules = [ '@react-leaflet', 'react-leaflet', '@tacc/core-components', + 'uuid', ].join('|'); module.exports = { @@ -32,7 +33,6 @@ module.exports = { '!src/**/*.spec.{js,jsx,ts,tsx}', // Exclude test utilities '!src/test/**/*', - '!src/testUtil.ts', '!src/__fixtures__/**/*', // Exclude secrets and other files '!src/secret_local*ts', @@ -156,7 +156,7 @@ module.exports = { // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], + setupFilesAfterEnv: ['/jest.setup.ts'], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, @@ -165,11 +165,14 @@ module.exports = { // snapshotSerializers: [], // The test environment that will be used for testing - testEnvironment: 'jsdom', + // https://mswjs.io/docs/migrations/1.x-to-2.x#requestresponsetextencoder-is-not-defined-jest; consider moving to vitest + testEnvironment: 'jest-fixed-jsdom', // Options that will be passed to the testEnvironment - // testEnvironmentOptions: {}, - + // Force JSDOM to import msw/node (see https://stackoverflow.com/questions/77399773/cannot-find-module-msw-node-from) + testEnvironmentOptions: { + customExportConditions: [''], + }, // Adds a location field to test results // testLocationInResults: false, diff --git a/react/jest.setup.ts b/react/jest.setup.ts new file mode 100644 index 00000000..da882238 --- /dev/null +++ b/react/jest.setup.ts @@ -0,0 +1,86 @@ +import { testDevConfiguration } from '@hazmapper/__fixtures__/appConfigurationFixture'; +import { server } from '@hazmapper/test/testUtil'; + +/***** A) Setup the configuration used for unit testing *****/ +jest.mock('@hazmapper/hooks/environment/getLocalAppConfiguration', () => ({ + getLocalAppConfiguration: jest.fn(() => testDevConfiguration), +})); + +/***** B) Ignore some known warnings/errors *****/ + +// List of `console.warn` messages that can be ignored +const ignoredWarnings = ['React Router Future Flag Warning']; + +// List of component-related errors to ignore +const ignoredErrors = [ + // Ignore request code errors as 500 is commong for our testing purposes + /Request failed with status code 500/, +]; + +// Helper function to determine if a `console.warn` message should be ignored +export function shouldIgnoreWarning(message: string): boolean { + return ignoredWarnings.some((warning) => message.includes(warning)); +} + +// Helper function to determine if a defaultProps error is from core-components +// Ingnoring till https://tacc-main.atlassian.net/browse/WI-208 is done +function isDefaultPropsErrorFromCoreComponents(args: any[]): boolean { + // Check if this is a defaultProps warning + const isDefaultPropsWarning = args[0]?.includes?.( + 'Support for defaultProps will be removed from function components' + ); + if (!isDefaultPropsWarning) return false; + + // Look through all arguments for stack trace containing @tacc/core-components + return args.some( + (arg) => typeof arg === 'string' && arg.includes('@tacc/core-components') + ); +} + +// Helper function to determine if a console.error message should be ignored +export function shouldIgnoreError(args: any[]): boolean { + // If it's a defaultProps warning from core-components, ignore it + if (isDefaultPropsErrorFromCoreComponents(args)) { + return true; + } + + // Check other error patterns + const messageStr = typeof args[0] === 'string' ? args[0] : args[0]?.message; + return ignoredErrors.some((pattern) => pattern.test(messageStr)); +} + +/***** C) Setup testing and also ensure that we are mocking things in tests *****/ + +beforeAll(() => { + // Establish mocking of APIs before all tests + server.listen({ onUnhandledRequest: 'error' }); + + const originalError = console.error; + + jest.spyOn(console, 'error').mockImplementation((...args: any[]) => { + if (shouldIgnoreError(args)) { + return; + } + originalError.apply(console, args); + }); + + const originalWarn = console.warn; + + jest.spyOn(console, 'warn').mockImplementation((...args: any[]) => { + // Check if contains the 500 status message as common for our testing purposes + // so not needed to be logged; + if (typeof args[0] === 'string' && shouldIgnoreWarning(args[0])) { + return; + } + originalWarn.apply(console, args); + }); +}); + +// Reset any runtime request handlers we may add during the tests +afterEach(() => server.resetHandlers()); + +// Clean up after the tests are finished +afterAll(() => { + server.close(); + jest.restoreAllMocks(); +}); diff --git a/react/package-lock.json b/react/package-lock.json index 272bf0e9..f4bf059d 100644 --- a/react/package-lock.json +++ b/react/package-lock.json @@ -49,6 +49,7 @@ "@testing-library/react": "^16.0.1", "@types/jest": "^29.0.5", "@types/leaflet": "^1.9.0", + "@types/nock": "^11.1.0", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", "@typescript-eslint/eslint-plugin": "^5.38.0", @@ -59,7 +60,9 @@ "identity-obj-proxy": "^3.0.0", "jest": "^29.0.5", "jest-environment-jsdom": "^29.0.5", + "jest-fixed-jsdom": "^0.0.8", "jest-transform-stub": "^2.0.0", + "msw": "^2.6.2", "rollup-plugin-visualizer": "^5.12.0", "ts-jest": "^29.0.5", "typescript": "^4.6.4", @@ -1852,6 +1855,43 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "dev": true, + "dependencies": { + "cookie": "^0.7.2" + } + }, + "node_modules/@bundled-es-modules/cookie/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, "node_modules/@changey/react-leaflet-markercluster": { "version": "4.0.0-rc1", "resolved": "https://registry.npmjs.org/@changey/react-leaflet-markercluster/-/react-leaflet-markercluster-4.0.0-rc1.tgz", @@ -3585,6 +3625,89 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead" }, + "node_modules/@inquirer/confirm": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.0.1.tgz", + "integrity": "sha512-6ycMm7k7NUApiMGfVc32yIPp28iPKxhGRMqoNDiUjq2RyTAkbs5Fx0TdzBqhabcKvniDdAAvHCmsRjnNfTsogw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.0.1", + "@inquirer/type": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.0.1.tgz", + "integrity": "sha512-KKTgjViBQUi3AAssqjUFMnMO3CM3qwCHvePV9EW+zTKGKafFGFF01sc1yOIYjLJ7QU52G/FbzKc+c01WLzXmVQ==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.7", + "@inquirer/type": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz", + "integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.0.tgz", + "integrity": "sha512-YYykfbw/lefC7yKj7nanzQXILM7r3suIvyFlCcMskc99axmsSewXWkAfXKwMbgxL76iAFVmRwmYdwNZNc8gjog==", + "dev": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -4360,6 +4483,23 @@ "react": ">=16" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.36.10", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.36.10.tgz", + "integrity": "sha512-GXrJgakgJW3DWKueebkvtYgGKkxA7s0u5B0P5syJM5rvQUnrpLPigvci8Hukl7yEM+sU06l+er2Fgvx/gmiRgg==", + "dev": true, + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5081,6 +5221,28 @@ "node": ">=10" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true + }, "node_modules/@phenomnomnominal/tsquery": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-5.0.1.tgz", @@ -6386,9 +6548,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", - "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", + "integrity": "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==", "engines": { "node": ">=14.0.0" } @@ -7604,9 +7766,9 @@ } }, "node_modules/@tacc/core-styles": { - "version": "2.37.0", - "resolved": "https://registry.npmjs.org/@tacc/core-styles/-/core-styles-2.37.0.tgz", - "integrity": "sha512-PaaRrIVNQ4yo8bSBC1vJWeTCQyKHs9MA1K2Jlgdyj0xMJqV8zxhiSWk1L2cM5C5RV9yEwWvyGBQtQ4zATQZw3Q==", + "version": "2.37.2", + "resolved": "https://registry.npmjs.org/@tacc/core-styles/-/core-styles-2.37.2.tgz", + "integrity": "sha512-Mj47VU6WBUrkaT48OWHgKBPeWgUS1vkKuGupXmUjH4SjeKj1nXAAXdCUP5WpVfKccsyWmESyprMuCOlVqIDEQA==", "bin": { "core-styles": "src/cli.js" }, @@ -9366,6 +9528,12 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, "node_modules/@types/detect-port": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.5.tgz", @@ -9588,6 +9756,16 @@ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", "optional": true }, + "node_modules/@types/nock": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-11.1.0.tgz", + "integrity": "sha512-jI/ewavBQ7X5178262JQR0ewicPAcJhXS/iFaNJl0VHLfyosZ/kwSrsa6VNQNSO8i9d8SqdRgOtZSOKJ/+iNMw==", + "deprecated": "This is a stub types definition. nock provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "nock": "*" + } + }, "node_modules/@types/node": { "version": "22.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", @@ -9699,6 +9877,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -10471,9 +10655,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/attr-accept": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.4.tgz", - "integrity": "sha512-2pA6xFIbdTUDCAwjN8nQwI+842VwzbDUXO2IYlpPXQIORgKnavorcr4Ce3rwh+zsNg9zK7QPsdvDj3Lum4WX4w==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", "engines": { "node": ">=4" } @@ -11033,9 +11217,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001677", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz", - "integrity": "sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==", + "version": "1.0.30001679", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001679.tgz", + "integrity": "sha512-j2YqID/YwpLnKzCmBOS4tlZdWprXm3ZmQLBH9ZBXFOhoxLA46fwyBvx6toCBWBmnuwUY/qB3kEU6gFx8qgCroA==", "funding": [ { "type": "opencollective", @@ -11176,6 +11360,15 @@ "@colors/colors": "1.5.0" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -11465,9 +11658,9 @@ "optional": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -12311,9 +12504,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.52", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.52.tgz", - "integrity": "sha512-xtoijJTZ+qeucLBDNztDOuQBE1ksqjvNjvqFoST3nGC7fSpqJ+X6BdTBaY5BHG+IhWWmpc6b/KfpeuEDupEPOQ==" + "version": "1.5.55", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", + "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==" }, "node_modules/emittery": { "version": "0.13.1", @@ -14043,6 +14236,15 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "node_modules/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/grid-index": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", @@ -14171,6 +14373,12 @@ "node": ">= 0.4" } }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -14776,6 +14984,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -15618,6 +15832,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-fixed-jsdom": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/jest-fixed-jsdom/-/jest-fixed-jsdom-0.0.8.tgz", + "integrity": "sha512-Pf7y5Px6MV8EwDtqJsYMJsiUi/Iu99hjMaqTrxGu/2j4kCz9wbz6iJ+NAIkbALKLlFxq7jpP8xJZbEVjCbih6A==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "jest-environment-jsdom": ">=28.0.0" + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -16311,6 +16537,12 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -16868,12 +17100,83 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/msw": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.6.2.tgz", + "integrity": "sha512-RdRgPvjfuzMIACkWv7VOVAeSRYMU3ofokLv1w0RsbFX960qnj/tFEyOFXY0G2GTUd9trA6rHuHciM/FKpBp6/A==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.36.5", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true + }, + "node_modules/msw/node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/murmurhash-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", "peer": true }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/nano-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", @@ -16925,6 +17228,20 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "optional": true }, + "node_modules/nock": { + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.5.tgz", + "integrity": "sha512-XKYnqUrCwXC8DGG1xX4YH5yNIrlh9c065uaMZZHUoeUUINTOyt+x/G+ezYk0Ft6ExSREVIs+qBJDK503viTfFA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-cmd": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/node-cmd/-/node-cmd-5.0.0.tgz", @@ -17213,9 +17530,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "engines": { "node": ">= 0.4" }, @@ -17423,6 +17740,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -19539,6 +19862,15 @@ "react-is": "^16.13.1" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/property-expr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", @@ -19569,10 +19901,13 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.10.0.tgz", + "integrity": "sha512-KSKHEbjAnpUuAUserOq0FxGXCUrzC3WniuSJhvdbs102rL55266ZcHBqLWOsG30spQMlPdpy7icATiAQehg/iA==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -20011,11 +20346,11 @@ } }, "node_modules/react-router": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", - "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz", + "integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==", "dependencies": { - "@remix-run/router": "1.20.0" + "@remix-run/router": "1.21.0" }, "engines": { "node": ">=14.0.0" @@ -20025,12 +20360,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", - "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.0.tgz", + "integrity": "sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==", "dependencies": { - "@remix-run/router": "1.20.0", - "react-router": "6.27.0" + "@remix-run/router": "1.21.0", + "react-router": "6.28.0" }, "engines": { "node": ">=14.0.0" @@ -21015,7 +21350,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "optional": true, + "devOptional": true, "engines": { "node": ">= 0.8" } @@ -21026,6 +21361,12 @@ "integrity": "sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==", "optional": true }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -23233,6 +23574,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yup": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", diff --git a/react/package.json b/react/package.json index 020600cd..c24d202e 100644 --- a/react/package.json +++ b/react/package.json @@ -73,6 +73,7 @@ "@testing-library/react": "^16.0.1", "@types/jest": "^29.0.5", "@types/leaflet": "^1.9.0", + "@types/nock": "^11.1.0", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", "@typescript-eslint/eslint-plugin": "^5.38.0", @@ -83,7 +84,9 @@ "identity-obj-proxy": "^3.0.0", "jest": "^29.0.5", "jest-environment-jsdom": "^29.0.5", + "jest-fixed-jsdom": "^0.0.8", "jest-transform-stub": "^2.0.0", + "msw": "^2.6.2", "rollup-plugin-visualizer": "^5.12.0", "ts-jest": "^29.0.5", "typescript": "^4.6.4", diff --git a/react/src/AppRouter.tsx b/react/src/AppRouter.tsx index d004d1c9..dab56191 100644 --- a/react/src/AppRouter.tsx +++ b/react/src/AppRouter.tsx @@ -15,8 +15,8 @@ import Login from './pages/Login/Login'; import Callback from './pages/Callback/Callback'; import StreetviewCallback from './pages/StreetviewCallback/StreetviewCallback'; import { RootState } from './redux/store'; -import { isTokenValid } from './utils/authUtils'; -import { useBasePath } from './hooks/environment'; +import { isTokenValid } from '@hazmapper/utils/authUtils'; +import { useBasePath } from '@hazmapper/hooks/environment'; interface ProtectedRouteProps { isAuthenticated: boolean; diff --git a/react/src/__fixtures__/appConfigurationFixture.ts b/react/src/__fixtures__/appConfigurationFixture.ts index 374d360b..affa8afb 100644 --- a/react/src/__fixtures__/appConfigurationFixture.ts +++ b/react/src/__fixtures__/appConfigurationFixture.ts @@ -1,8 +1,4 @@ -import { - GeoapiBackendEnvironment, - AppConfiguration, - MapillaryConfiguration, -} from '../types'; +import { AppConfiguration, MapillaryConfiguration } from '@hazmapper/types'; export const mapillaryConfig: MapillaryConfiguration = { authUrl: 'https://www.mapillary.com/connect', @@ -16,11 +12,11 @@ export const mapillaryConfig: MapillaryConfiguration = { clientToken: '', }; -export const localDevConfiguration: AppConfiguration = { - basePath: '/', - geoapiBackend: GeoapiBackendEnvironment.Local, - geoapiUrl: 'http://localhost:8888', - designsafePortalUrl: 'https://designsafeci-dev.tacc.utexas.edu', +export const testDevConfiguration: AppConfiguration = { + basePath: '/test', + geoapiUrl: 'https://geoapi.unittest', + designsafePortalUrl: 'https://designsafeci.unittest', + tapisUrl: 'https://tapis.io.unittest', mapillary: mapillaryConfig, - taggitUrl: 'http://localhost:4200/taggit-staging', + taggitUrl: 'https://taggit.unittest', }; diff --git a/react/src/__fixtures__/authStateFixtures.ts b/react/src/__fixtures__/authStateFixtures.ts index 4d6215b1..eeaaa747 100644 --- a/react/src/__fixtures__/authStateFixtures.ts +++ b/react/src/__fixtures__/authStateFixtures.ts @@ -1,4 +1,4 @@ -import { AuthState } from '../types'; +import { AuthState } from '@hazmapper/types'; // Convert the timestamp to a Date object const expiresAtDate = new Date(3153600000000); //2070 diff --git a/react/src/__fixtures__/systemsFixture.ts b/react/src/__fixtures__/systemsFixture.ts new file mode 100644 index 00000000..e168fa84 --- /dev/null +++ b/react/src/__fixtures__/systemsFixture.ts @@ -0,0 +1,47 @@ +export const systems = { + result: [ + { + id: 'project-1234abcd-ab12-ab12-ab12-123456789abc', + systemType: 'LINUX', + owner: 'wma_prtl', + host: 'cloud.corral.tacc.utexas.edu', + effectiveUserId: 'tg458981', + defaultAuthnMethod: 'PKI_KEYS', + canExec: 'false', + parentId: null, + }, + { + id: 'designsafe.storage.default', + systemType: 'LINUX', + owner: 'wma_prtl', + host: 'cloud.data.tacc.utexas.edu', + effectiveUserId: 'username', + defaultAuthnMethod: 'PKI_KEYS', + canExec: 'false', + parentId: null, + }, + { + id: 'designsafe.storage.community', + systemType: 'LINUX', + owner: 'wma_prtl', + host: 'cloud.data.tacc.utexas.edu', + effectiveUserId: 'tg458981', + defaultAuthnMethod: 'PKI_KEYS', + canExec: 'false', + parentId: null, + }, + ], + status: 'success', + message: 'TAPIS_FOUND Systems found: 3 systems', + version: '1.7.0', + commit: '2eee8fa0', + build: '2024-09-09T14:15:04Z', + metadata: { + recordCount: 3, + recordLimit: 100, + recordsSkipped: 0, + orderBy: null, + startAfter: null, + totalCount: -1, + }, +}; diff --git a/react/src/components/CreateMapModal/CreateMapModal.test.tsx b/react/src/components/CreateMapModal/CreateMapModal.test.tsx index 2e48cdfb..8000c9f7 100644 --- a/react/src/components/CreateMapModal/CreateMapModal.test.tsx +++ b/react/src/components/CreateMapModal/CreateMapModal.test.tsx @@ -1,15 +1,15 @@ import React, { act } from 'react'; -import { - render, - cleanup, - fireEvent, - screen, - waitFor, -} from '@testing-library/react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import store from '../../redux/store'; +import { http, HttpResponse } from 'msw'; import CreateMapModal from './CreateMapModal'; import { BrowserRouter as Router } from 'react-router-dom'; -import { QueryClient, QueryClientProvider } from 'react-query'; +import { QueryClientProvider } from 'react-query'; +import { testQueryClient, server } from '@hazmapper/test/testUtil'; +import { testDevConfiguration } from '@hazmapper/__fixtures__/appConfigurationFixture'; +import { projectMock } from '@hazmapper/__fixtures__/projectFixtures'; jest.mock('@hazmapper/hooks/user/useAuthenticatedUser', () => ({ __esModule: true, @@ -20,22 +20,6 @@ jest.mock('@hazmapper/hooks/user/useAuthenticatedUser', () => ({ }), })); -jest.mock('@hazmapper/hooks/projects/useCreateProject', () => ({ - __esModule: true, - default: () => ({ - mutate: jest.fn((data, { onSuccess, onError }) => { - if (data.name === 'Error Map') { - // Simulate a submission error with a 500 status code - onError({ response: { status: 500 } }); - } else { - // Simulate successful project creation - onSuccess({ uuid: '123' }); - } - }), - isLoading: false, - }), -})); - const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ @@ -44,25 +28,22 @@ jest.mock('react-router-dom', () => ({ })); const toggleMock = jest.fn(); -const queryClient = new QueryClient(); const renderComponent = async (isOpen = true) => { await act(async () => { render( - - - - - + + + + + + + ); }); }; describe('CreateMapModal', () => { - afterEach(() => { - cleanup(); - }); - test('renders the modal when open', async () => { await renderComponent(); await waitFor(() => { @@ -71,6 +52,12 @@ describe('CreateMapModal', () => { }); test('submits form data successfully', async () => { + server.use( + http.post(`${testDevConfiguration.geoapiUrl}/projects/`, () => { + return HttpResponse.json(projectMock, { status: 200 }); + }) + ); + await renderComponent(); await act(async () => { fireEvent.change(screen.getByTestId('name-input'), { @@ -86,11 +73,19 @@ describe('CreateMapModal', () => { }); await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith('/project/123'); + expect(mockNavigate).toHaveBeenCalledWith(`/project/${projectMock.uuid}`); }); }); test('displays error message on submission error', async () => { + server.use( + http.post(`${testDevConfiguration.geoapiUrl}/projects/`, async () => { + return new HttpResponse(null, { + status: 500, + }); + }) + ); + await renderComponent(); await act(async () => { fireEvent.change(screen.getByTestId('name-input'), { diff --git a/react/src/hooks/environment/getLocalAppConfiguration.ts b/react/src/hooks/environment/getLocalAppConfiguration.ts new file mode 100644 index 00000000..a0272d00 --- /dev/null +++ b/react/src/hooks/environment/getLocalAppConfiguration.ts @@ -0,0 +1,35 @@ +// TODO_REACT consider dynamically importing local configuration but then we would need to refactor things as initial configuration is async (context?) +import { localDevelopmentConfiguration } from '@hazmapper/secret_local'; +import { getGeoapiUrl, getDesignsafePortalUrl } from './utils'; +import { + AppConfiguration, + MapillaryConfiguration, + DesignSafePortalEnvironment, +} from '@hazmapper/types'; + +/** + * Get app configuration for when running locally + * + * Note: this is mocked when running unit tests + */ +export const getLocalAppConfiguration = ( + basePath: string, + mapillaryConfig: MapillaryConfiguration +): AppConfiguration => { + const appConfig: AppConfiguration = { + basePath: basePath, + geoapiUrl: getGeoapiUrl(localDevelopmentConfiguration.geoapiBackend), + designsafePortalUrl: getDesignsafePortalUrl( + DesignSafePortalEnvironment.Dev + ), + tapisUrl: 'https://designsafe.tapis.io', + mapillary: mapillaryConfig, + taggitUrl: origin + '/taggit-staging', + }; + appConfig.mapillary.clientId = '5156692464392931'; + appConfig.mapillary.clientSecret = + 'MLY|5156692464392931|6be48c9f4074f4d486e0c42a012b349f'; + appConfig.mapillary.clientToken = + 'MLY|5156692464392931|4f1118aa1b06f051a44217cb56bedf79'; + return appConfig; +}; diff --git a/react/src/hooks/environment/useAppConfiguration.ts b/react/src/hooks/environment/useAppConfiguration.ts index 5093c6bb..edb52679 100644 --- a/react/src/hooks/environment/useAppConfiguration.ts +++ b/react/src/hooks/environment/useAppConfiguration.ts @@ -1,53 +1,24 @@ import { useMemo } from 'react'; -// TODO_REACT consider dynamically importing local configuration but then we would need to refactor things as initial configuration is async (context?) -import { localDevelopmentConfiguration } from '../../secret_local'; - import { AppConfiguration, MapillaryConfiguration, GeoapiBackendEnvironment, DesignSafePortalEnvironment, } from '@hazmapper/types'; +import { getGeoapiUrl, getDesignsafePortalUrl } from './utils'; import useBasePath from './useBasePath'; +import { getLocalAppConfiguration } from './getLocalAppConfiguration'; /** - * Retrieves the GeoAPI URL based on the provided backend environment. + * A hook that provides environment-specific application configuration based on the current hostname and path. + * + * This hook determines the appropriate configuration for various environments: + * - Local development (localhost or hazmapper.local) + * - Staging (/staging path on hazmapper.tacc.utexas.edu) + * - Development (/dev path on hazmapper.tacc.utexas.edu) + * - Production (hazmapper.tacc.utexas.edu without path prefix) */ -function getGeoapiUrl(backend: GeoapiBackendEnvironment): string { - switch (backend) { - case GeoapiBackendEnvironment.Local: - return 'http://localhost:8888'; - case GeoapiBackendEnvironment.Experimental: - return 'https://hazmapper.tacc.utexas.edu/geoapi-experimental'; - case GeoapiBackendEnvironment.Dev: - return 'https://hazmapper.tacc.utexas.edu/geoapi-dev'; - case GeoapiBackendEnvironment.Staging: - return 'https://hazmapper.tacc.utexas.edu/geoapi-staging'; - case GeoapiBackendEnvironment.Production: - return 'https://hazmapper.tacc.utexas.edu/geoapi'; - default: - throw new Error( - 'Unsupported TARGET/GEOAPI_BACKEND Type. Please check the .env file.' - ); - } -} - -/** - * Retrieves the DesignSafe portal URL based on the provided backend environment. - */ -function getDesignsafePortalUrl(backend: DesignSafePortalEnvironment): string { - if (backend === DesignSafePortalEnvironment.Production) { - return 'https://www.designsafe-ci.org'; - } else if (backend === DesignSafePortalEnvironment.Next) { - return 'https://designsafeci-next.tacc.utexas.edu'; - } else if (backend === DesignSafePortalEnvironment.Dev) { - return 'https://designsafeci-dev.tacc.utexas.edu'; - } else { - throw new Error('Unsupported DS environment'); - } -} - export const useAppConfiguration = (): AppConfiguration => { const basePath = useBasePath(); @@ -68,33 +39,18 @@ export const useAppConfiguration = (): AppConfiguration => { }; if (/^localhost/.test(hostname) || /^hazmapper.local/.test(hostname)) { - const appConfig: AppConfiguration = { - basePath: basePath, - geoapiBackend: localDevelopmentConfiguration.geoapiBackend, - geoapiUrl: getGeoapiUrl(localDevelopmentConfiguration.geoapiBackend), - designsafePortalUrl: getDesignsafePortalUrl( - DesignSafePortalEnvironment.Dev - ), - mapillary: mapillaryConfig, - taggitUrl: origin + '/taggit-staging', - }; - appConfig.mapillary.clientId = '5156692464392931'; - appConfig.mapillary.clientSecret = - 'MLY|5156692464392931|6be48c9f4074f4d486e0c42a012b349f'; - appConfig.mapillary.clientToken = - 'MLY|5156692464392931|4f1118aa1b06f051a44217cb56bedf79'; - return appConfig; + return getLocalAppConfiguration(basePath, mapillaryConfig); } else if ( /^hazmapper.tacc.utexas.edu/.test(hostname) && pathname.startsWith('/staging') ) { const appConfig: AppConfiguration = { basePath: basePath, - geoapiBackend: GeoapiBackendEnvironment.Staging, geoapiUrl: getGeoapiUrl(GeoapiBackendEnvironment.Staging), designsafePortalUrl: getDesignsafePortalUrl( DesignSafePortalEnvironment.Dev ), + tapisUrl: 'https://designsafe.tapis.io', mapillary: mapillaryConfig, taggitUrl: origin + '/taggit-staging', }; @@ -111,11 +67,11 @@ export const useAppConfiguration = (): AppConfiguration => { ) { const appConfig: AppConfiguration = { basePath: basePath, - geoapiBackend: GeoapiBackendEnvironment.Dev, geoapiUrl: getGeoapiUrl(GeoapiBackendEnvironment.Dev), designsafePortalUrl: getDesignsafePortalUrl( DesignSafePortalEnvironment.Dev ), + tapisUrl: 'https://designsafe.tapis.io', mapillary: mapillaryConfig, taggitUrl: origin + '/taggit-dev', }; @@ -130,11 +86,11 @@ export const useAppConfiguration = (): AppConfiguration => { } else if (/^hazmapper.tacc.utexas.edu/.test(hostname)) { const appConfig: AppConfiguration = { basePath: basePath, - geoapiBackend: GeoapiBackendEnvironment.Production, geoapiUrl: getGeoapiUrl(GeoapiBackendEnvironment.Production), designsafePortalUrl: getDesignsafePortalUrl( DesignSafePortalEnvironment.Production ), + tapisUrl: 'https://designsafe.tapis.io', mapillary: mapillaryConfig, taggitUrl: origin + '/taggit', }; diff --git a/react/src/hooks/environment/utils.ts b/react/src/hooks/environment/utils.ts new file mode 100644 index 00000000..eb674a0c --- /dev/null +++ b/react/src/hooks/environment/utils.ts @@ -0,0 +1,43 @@ +import { + GeoapiBackendEnvironment, + DesignSafePortalEnvironment, +} from '@hazmapper/types'; + +/** + * Retrieves the GeoAPI URL based on the provided backend environment. + */ +export function getGeoapiUrl(backend: GeoapiBackendEnvironment): string { + switch (backend) { + case GeoapiBackendEnvironment.Local: + return 'http://localhost:8888'; + case GeoapiBackendEnvironment.Experimental: + return 'https://hazmapper.tacc.utexas.edu/geoapi-experimental'; + case GeoapiBackendEnvironment.Dev: + return 'https://hazmapper.tacc.utexas.edu/geoapi-dev'; + case GeoapiBackendEnvironment.Staging: + return 'https://hazmapper.tacc.utexas.edu/geoapi-staging'; + case GeoapiBackendEnvironment.Production: + return 'https://hazmapper.tacc.utexas.edu/geoapi'; + default: + throw new Error( + 'Unsupported TARGET/GEOAPI_BACKEND Type. Please check the .env file.' + ); + } +} + +/** + * Retrieves the DesignSafe portal URL based on the provided backend environment. + */ +export function getDesignsafePortalUrl( + backend: DesignSafePortalEnvironment +): string { + if (backend === DesignSafePortalEnvironment.Production) { + return 'https://www.designsafe-ci.org'; + } else if (backend === DesignSafePortalEnvironment.Next) { + return 'https://designsafeci-next.tacc.utexas.edu'; + } else if (backend === DesignSafePortalEnvironment.Dev) { + return 'https://designsafeci-dev.tacc.utexas.edu'; + } else { + throw new Error('Unsupported DS environment'); + } +} diff --git a/react/src/pages/Callback/Callback.test.tsx b/react/src/pages/Callback/Callback.test.tsx index 2484b03e..6edc3356 100644 --- a/react/src/pages/Callback/Callback.test.tsx +++ b/react/src/pages/Callback/Callback.test.tsx @@ -1,17 +1,9 @@ import React from 'react'; -import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; -import { render } from '@testing-library/react'; -import store from '../../redux/store'; +import { renderInTest } from '@hazmapper/test/testUtil'; import Callback from './Callback'; test('renders callback', async () => { - const { getByText } = render( - - - - - - ); + const { getByText } = renderInTest(, '/callback'); + expect(getByText(/Logging in/)).toBeDefined(); }); diff --git a/react/src/pages/Login/Login.test.tsx b/react/src/pages/Login/Login.test.tsx index f7a13d8c..81f2a541 100644 --- a/react/src/pages/Login/Login.test.tsx +++ b/react/src/pages/Login/Login.test.tsx @@ -1,11 +1,7 @@ import React from 'react'; -import { render, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import Login from './Login'; -import { Provider } from 'react-redux'; -import store from '../../redux/store'; -import { QueryClientProvider } from 'react-query'; -import { testQueryClient } from '../../testUtil'; -import { MemoryRouter } from 'react-router'; +import { renderInTest } from '../../test/testUtil'; beforeAll(() => { const mockLocation = { @@ -25,15 +21,7 @@ afterAll(() => { }); test('renders login', async () => { - const { getByText } = render( - - - - - - - - ); + const { getByText } = renderInTest(); expect(getByText(/Logging in/)).toBeDefined(); await waitFor(() => { diff --git a/react/src/pages/MainMenu/MainMenu.test.tsx b/react/src/pages/MainMenu/MainMenu.test.tsx index 8a8a6c05..4bcfd5ed 100644 --- a/react/src/pages/MainMenu/MainMenu.test.tsx +++ b/react/src/pages/MainMenu/MainMenu.test.tsx @@ -1,21 +1,22 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import MainMenu from './MainMenu'; -import { QueryClientProvider } from 'react-query'; -import { testQueryClient } from '../../testUtil'; -import { Provider } from 'react-redux'; -import store from '../../redux/store'; -import { BrowserRouter as Router } from 'react-router-dom'; +import { renderInTest } from '@hazmapper/test/testUtil'; -test('renders menu', () => { - const { getByText } = render( - - - - - - - - ); - expect(getByText(/Main Menu/)).toBeDefined(); +jest.mock('@hazmapper/hooks/user/useAuthenticatedUser', () => ({ + __esModule: true, + default: () => ({ + data: { username: 'mockUser' }, + isLoading: false, + error: null, + }), +})); + +test('renders menu', async () => { + const { getByText } = renderInTest(); + + // Wait for the "Main Menu" text to be rendered and then check it + await waitFor(() => { + expect(getByText(/Main Menu/)).toBeDefined(); + }); }); diff --git a/react/src/redux/authSlice.ts b/react/src/redux/authSlice.ts index cee8bb2e..9988eec3 100644 --- a/react/src/redux/authSlice.ts +++ b/react/src/redux/authSlice.ts @@ -4,7 +4,7 @@ import { setAuthenticatedUserFromLocalStorage, removeAuthenticatedUserFromLocalStorage, } from '../utils/authUtils'; -import { AuthenticatedUser, AuthToken } from '../types'; +import { AuthenticatedUser, AuthToken } from '@hazmapper/types'; // check local storage for our initial state const initialState = getAuthenticatedUserFromLocalStorage(); diff --git a/react/src/requests.test.ts b/react/src/requests.test.ts index 95f41bda..feb4c190 100644 --- a/react/src/requests.test.ts +++ b/react/src/requests.test.ts @@ -1,5 +1,5 @@ import { getHeaders } from './requests'; -import { ApiService } from './types'; +import { ApiService } from '@hazmapper/types'; import { authenticatedUser, unauthenticatedUser, diff --git a/react/src/requests.ts b/react/src/requests.ts index a48c7ccd..ee03e1a4 100644 --- a/react/src/requests.ts +++ b/react/src/requests.ts @@ -8,11 +8,10 @@ import { UseMutationOptions, QueryKey, } from 'react-query'; -import { - useAppConfiguration, - useEnsureAuthenticatedUserHasValidTapisToken, -} from './hooks'; -import { ApiService, AppConfiguration, AuthState } from './types'; +import { useAppConfiguration } from '@hazmapper/hooks'; + +import { useEnsureAuthenticatedUserHasValidTapisToken } from '@hazmapper/hooks'; +import { ApiService, AppConfiguration, AuthState } from '@hazmapper/types'; import { v4 as uuidv4 } from 'uuid'; function getBaseApiUrl( @@ -25,7 +24,7 @@ function getBaseApiUrl( case ApiService.DesignSafe: return configuration.designsafePortalUrl; case ApiService.Tapis: - return 'https://designsafe.tapis.io'; + return configuration.tapisUrl; default: throw new Error('Unsupported api service Type.'); } diff --git a/react/src/test/handlers.ts b/react/src/test/handlers.ts new file mode 100644 index 00000000..7f7c3181 --- /dev/null +++ b/react/src/test/handlers.ts @@ -0,0 +1,48 @@ +import { http, HttpResponse } from 'msw'; +import { testDevConfiguration } from '@hazmapper/__fixtures__/appConfigurationFixture'; +import { systems } from '@hazmapper/__fixtures__/systemsFixture'; + +// ArcGIS tiles GET +export const arcgis_tiles = http.get('https://tiles.arcgis.com/*', () => { + return HttpResponse.text('dummy'); +}); + +// DesignSafe Projects GET +export const designsafe_projects = http.get( + `${testDevConfiguration.designsafePortalUrl}/api/projects/v2/`, + () => HttpResponse.json({ results: [] }, { status: 200 }) +); + +// GeoAPI Projects GET +export const geoapi_projects_list = http.get( + `${testDevConfiguration.geoapiUrl}/projects/`, + () => HttpResponse.json({}, { status: 200 }) +); + +// Tapis Systems GET +export const tapis_systems = http.get( + `${testDevConfiguration.tapisUrl}/v3/systems/`, + () => HttpResponse.json(systems, { status: 200 }) +); + +// Tapis Files GET +export const tapis_files_listing = http.get( + `${testDevConfiguration.tapisUrl}/v3/files/listing/*`, + () => + HttpResponse.json( + { + status: 'success', + result: [], + }, + { status: 200 } + ) +); + +// Export all handlers together for server setup +export const defaultHandlers = [ + arcgis_tiles, + designsafe_projects, + geoapi_projects_list, + tapis_files_listing, + tapis_systems, +]; diff --git a/react/src/test/testUtil.tsx b/react/src/test/testUtil.tsx new file mode 100644 index 00000000..c03a4985 --- /dev/null +++ b/react/src/test/testUtil.tsx @@ -0,0 +1,33 @@ +import React, { ReactElement } from 'react'; +import { render } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { MemoryRouter } from 'react-router-dom'; +import { setupServer } from 'msw/node'; +import store from '@hazmapper/redux/store'; +import { defaultHandlers } from '@hazmapper/test/handlers'; + +export const server = setupServer(...defaultHandlers); + +export const testQueryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + staleTime: 0, + useErrorBoundary: true, + }, + }, +}); + +export function renderInTest(children: ReactElement, path = '/') { + return render( + + + + {children} + + + + ); +} diff --git a/react/src/testUtil.ts b/react/src/testUtil.ts deleted file mode 100644 index 4c522729..00000000 --- a/react/src/testUtil.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { QueryClient } from 'react-query'; - -export const testQueryClient = new QueryClient({ - defaultOptions: { queries: { retry: false, cacheTime: Infinity } }, -}); diff --git a/react/src/types/environment.ts b/react/src/types/environment.ts index 6e398eff..893d723d 100644 --- a/react/src/types/environment.ts +++ b/react/src/types/environment.ts @@ -69,15 +69,15 @@ export interface AppConfiguration { /** Base URL path for the application. */ basePath: string; - /* The type of backend environment */ - geoapiBackend: GeoapiBackendEnvironment; - /** URL for the GeoAPI service. */ geoapiUrl: string; /** URL for the DesignSafe portal and API. */ designsafePortalUrl: string; + /** URL for Tapis */ + tapisUrl: string; + /** Mapillary related configuration */ mapillary: MapillaryConfiguration; diff --git a/react/src/types/projects.ts b/react/src/types/projects.ts index 2c681d2f..5ab6de60 100644 --- a/react/src/types/projects.ts +++ b/react/src/types/projects.ts @@ -1,6 +1,6 @@ export interface Project { id: number; - uuid?: string; + uuid: string; name: string; description: string; public: boolean; diff --git a/react/src/utils/authUtils.test.ts b/react/src/utils/authUtils.test.ts index 0a15eaaf..06303290 100644 --- a/react/src/utils/authUtils.test.ts +++ b/react/src/utils/authUtils.test.ts @@ -5,7 +5,7 @@ import { removeAuthenticatedUserFromLocalStorage, AUTH_KEY, } from './authUtils'; -import { AuthState, AuthToken } from '../types'; +import { AuthState, AuthToken } from '@hazmapper/types'; describe('Auth Utils', () => { describe('isTokenValid', () => { diff --git a/react/src/utils/authUtils.ts b/react/src/utils/authUtils.ts index 3a8c9146..72f872ee 100644 --- a/react/src/utils/authUtils.ts +++ b/react/src/utils/authUtils.ts @@ -1,4 +1,4 @@ -import { AuthToken, AuthState } from '../types'; +import { AuthToken, AuthState } from '@hazmapper/types'; export const AUTH_KEY = 'authV3'; diff --git a/react/tsconfig.json b/react/tsconfig.json index 939d8f8a..c439a5be 100644 --- a/react/tsconfig.json +++ b/react/tsconfig.json @@ -21,6 +21,6 @@ "@hazmapper/*": ["src/*"] } }, - "include": ["src"], + "include": ["src", "jest.setup.ts"], "references": [{ "path": "./tsconfig.node.json" }] }