From 675e752919c1df50c1796b6d388ec69451bb1016 Mon Sep 17 00:00:00 2001 From: Benjamin Poreh Date: Wed, 18 Sep 2024 14:33:31 -0400 Subject: [PATCH 01/26] EDSC-4238: Initial implemenation of the highlighting of text --- .../GranuleResults/GranuleResultsItem.jsx | 62 ++++++++++++++++++- .../GranuleResults/GranuleResultsItem.scss | 7 +++ .../GranuleResults/GranuleResultsListItem.jsx | 2 +- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx index 19b2ad8312..2f1e30259a 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx @@ -1,5 +1,6 @@ import React, { forwardRef } from 'react' import { PropTypes } from 'prop-types' +import { parse } from 'qs' import classNames from 'classnames' import { LinkContainer } from 'react-router-bootstrap' @@ -26,6 +27,64 @@ import PortalFeatureContainer from '../../containers/PortalFeatureContainer/Port import './GranuleResultsItem.scss' +/** + * Highlight substring if it's in being searched for in Granule Id(s) Filter + */ +const highlightSubstring = ( + location, + title +) => { + const { search } = location + const params = parse(search, { + ignoreQueryPrefix: true, + parseArrays: false + }) + const { pg = {} } = params + + const pgParams = pg[0] + + const { id: substring } = pgParams + + if (!substring) { + return title + } + + const splitStars = substring.split(/[*]+/) + + const rebuiltTitle = [] + + let trimmedTitle = title + + splitStars.forEach((splitStarsVal) => { + if (splitStarsVal !== '') { + const regexTerm = `(${splitStarsVal.replace('?', '.')})` + const regexResults = trimmedTitle.match(regexTerm) + + // Slices the found regex expression off + // adds that match result to the list of strings to be bolded + if (regexResults) { + const slicedOffNormalText = trimmedTitle.slice(0, regexResults.index) + rebuiltTitle.push(slicedOffNormalText) + + trimmedTitle = trimmedTitle.slice( + regexResults[0].length + regexResults.index, + trimmedTitle.length + ) + + rebuiltTitle.push({regexResults[0]}) + } + } + }) + + if (trimmedTitle.length !== 0) { + rebuiltTitle.push(trimmedTitle) + } + + console.log(rebuiltTitle) + + return rebuiltTitle +} + /** * Renders GranuleResultsItem. * @param {Object} props - The props passed into the component. @@ -194,7 +253,8 @@ const GranuleResultsItem = forwardRef(({

- {title} + {highlightSubstring(location, title)} + {/* {title} */}

Date: Wed, 18 Sep 2024 14:49:08 -0400 Subject: [PATCH 02/26] EDSC-4238: finished implementation using Highlighter --- package-lock.json | 49 +++++++++++++++++++ package.json | 1 + .../GranuleResults/GranuleResultsItem.jsx | 48 ++++++------------ 3 files changed, 65 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index e2c968f5a9..c62523c27e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,6 +103,7 @@ "react-datetime": "3.2.0", "react-dom": "^17.0.2", "react-helmet": "^6.1.0", + "react-highlight-words": "^0.20.0", "react-icons": "^4.3.1", "react-input-range": "^1.3.0", "react-leaflet": "^4.2.0", @@ -17055,6 +17056,12 @@ "node": ">=8" } }, + "node_modules/highlight-words-core": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/highlight-words-core/-/highlight-words-core-1.2.2.tgz", + "integrity": "sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==", + "license": "MIT" + }, "node_modules/history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", @@ -27200,6 +27207,26 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "node_modules/react-highlight-words": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/react-highlight-words/-/react-highlight-words-0.20.0.tgz", + "integrity": "sha512-asCxy+jCehDVhusNmCBoxDf2mm1AJ//D+EzDx1m5K7EqsMBIHdZ5G4LdwbSEXqZq1Ros0G0UySWmAtntSph7XA==", + "license": "MIT", + "dependencies": { + "highlight-words-core": "^1.2.0", + "memoize-one": "^4.0.0", + "prop-types": "^15.5.8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" + } + }, + "node_modules/react-highlight-words/node_modules/memoize-one": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.3.tgz", + "integrity": "sha512-QmpUu4KqDmX0plH4u+tf0riMc1KHE1+lw95cMrLlXQAFOx/xnBtwhZ52XJxd9X2O6kwKBqX32kmhbhlobD0cuw==", + "license": "MIT" + }, "node_modules/react-icons": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", @@ -45292,6 +45319,11 @@ "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==" }, + "highlight-words-core": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/highlight-words-core/-/highlight-words-core-1.2.2.tgz", + "integrity": "sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==" + }, "history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", @@ -52966,6 +52998,23 @@ } } }, + "react-highlight-words": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/react-highlight-words/-/react-highlight-words-0.20.0.tgz", + "integrity": "sha512-asCxy+jCehDVhusNmCBoxDf2mm1AJ//D+EzDx1m5K7EqsMBIHdZ5G4LdwbSEXqZq1Ros0G0UySWmAtntSph7XA==", + "requires": { + "highlight-words-core": "^1.2.0", + "memoize-one": "^4.0.0", + "prop-types": "^15.5.8" + }, + "dependencies": { + "memoize-one": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.3.tgz", + "integrity": "sha512-QmpUu4KqDmX0plH4u+tf0riMc1KHE1+lw95cMrLlXQAFOx/xnBtwhZ52XJxd9X2O6kwKBqX32kmhbhlobD0cuw==" + } + } + }, "react-icons": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", diff --git a/package.json b/package.json index 5428a78469..7a316fc1e9 100644 --- a/package.json +++ b/package.json @@ -178,6 +178,7 @@ "react-datetime": "3.2.0", "react-dom": "^17.0.2", "react-helmet": "^6.1.0", + "react-highlight-words": "^0.20.0", "react-icons": "^4.3.1", "react-input-range": "^1.3.0", "react-leaflet": "^4.2.0", diff --git a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx index 2f1e30259a..f9e7716a71 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx @@ -5,6 +5,8 @@ import classNames from 'classnames' import { LinkContainer } from 'react-router-bootstrap' +import Highlighter from 'react-highlight-words' + import { AlertInformation } from '@edsc/earthdata-react-icons/horizon-design-system/earthdata/ui' import { Minus, @@ -30,10 +32,7 @@ import './GranuleResultsItem.scss' /** * Highlight substring if it's in being searched for in Granule Id(s) Filter */ -const highlightSubstring = ( - location, - title -) => { +const getSearchWords = (location) => { const { search } = location const params = parse(search, { ignoreQueryPrefix: true, @@ -46,43 +45,23 @@ const highlightSubstring = ( const { id: substring } = pgParams if (!substring) { - return title + return [] } - const splitStars = substring.split(/[*]+/) - - const rebuiltTitle = [] + const splitStars = substring.split('*') - let trimmedTitle = title + const searchTerms = [] splitStars.forEach((splitStarsVal) => { if (splitStarsVal !== '') { - const regexTerm = `(${splitStarsVal.replace('?', '.')})` - const regexResults = trimmedTitle.match(regexTerm) - - // Slices the found regex expression off - // adds that match result to the list of strings to be bolded - if (regexResults) { - const slicedOffNormalText = trimmedTitle.slice(0, regexResults.index) - rebuiltTitle.push(slicedOffNormalText) - - trimmedTitle = trimmedTitle.slice( - regexResults[0].length + regexResults.index, - trimmedTitle.length - ) - - rebuiltTitle.push({regexResults[0]}) - } + const regexTerm = RegExp(`(${splitStarsVal.replaceAll('?', '.')})`) + searchTerms.push(regexTerm) } }) - if (trimmedTitle.length !== 0) { - rebuiltTitle.push(trimmedTitle) - } - - console.log(rebuiltTitle) + console.log(searchTerms) - return rebuiltTitle + return searchTerms } /** @@ -253,8 +232,11 @@ const GranuleResultsItem = forwardRef(({

- {highlightSubstring(location, title)} - {/* {title} */} +

Date: Wed, 18 Sep 2024 15:14:24 -0400 Subject: [PATCH 03/26] EDSC-4238: Added the ability to have multiple search filters --- .../GranuleResults/GranuleResultsItem.jsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx index f9e7716a71..0a1a35085e 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx @@ -48,15 +48,23 @@ const getSearchWords = (location) => { return [] } - const splitStars = substring.split('*') - const searchTerms = [] - splitStars.forEach((splitStarsVal) => { - if (splitStarsVal !== '') { - const regexTerm = RegExp(`(${splitStarsVal.replaceAll('?', '.')})`) - searchTerms.push(regexTerm) + substring.split(',').forEach((initialSearchTerm) => { + let splitStars = initialSearchTerm.split('*') + + // Remove the first and last stars if they are there + if (splitStars[0] === '') { + splitStars = splitStars.slice(1) + } + + if (splitStars[splitStars.length - 1] === '') { + splitStars = splitStars.slice(0, splitStars.length - 1) } + + const searchTerm = splitStars.join('.+') + + searchTerms.push(RegExp(`(${searchTerm.replaceAll('?', '.')})`)) }) console.log(searchTerms) From 35d017a0b7f1c24288f39f2e2096a5de3764b5f0 Mon Sep 17 00:00:00 2001 From: Benjamin Poreh Date: Tue, 24 Sep 2024 11:09:50 -0400 Subject: [PATCH 04/26] EDSC-4238: moved props from redux into the GranuleResultsItem instead of parsing the location. --- .../GranuleResults/GranuleResultsBody.jsx | 7 +++-- .../GranuleResults/GranuleResultsItem.jsx | 31 +++++-------------- .../GranuleResults/GranuleResultsList.jsx | 4 +++ .../GranuleResults/GranuleResultsListBody.jsx | 3 ++ .../GranuleResults/GranuleResultsListItem.jsx | 3 ++ 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/static/src/js/components/GranuleResults/GranuleResultsBody.jsx b/static/src/js/components/GranuleResults/GranuleResultsBody.jsx index 335a22c6a0..3233d9c0d0 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsBody.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsBody.jsx @@ -71,7 +71,8 @@ const GranuleResultsBody = ({ } = granuleSearchResults const { - excludedGranuleIds = [] + excludedGranuleIds = [], + readableGranuleName = [''] } = granuleQuery const granuleIds = getGranuleIds({ @@ -183,6 +184,7 @@ const GranuleResultsBody = ({ > { - const { search } = location - const params = parse(search, { - ignoreQueryPrefix: true, - parseArrays: false - }) - const { pg = {} } = params - - const pgParams = pg[0] - - const { id: substring } = pgParams - - if (!substring) { - return [] - } - +const getSearchWords = (readableGranuleName) => { const searchTerms = [] - substring.split(',').forEach((initialSearchTerm) => { + readableGranuleName.forEach((initialSearchTerm) => { let splitStars = initialSearchTerm.split('*') // Remove the first and last stars if they are there @@ -67,8 +51,6 @@ const getSearchWords = (location) => { searchTerms.push(RegExp(`(${searchTerm.replaceAll('?', '.')})`)) }) - console.log(searchTerms) - return searchTerms } @@ -86,6 +68,7 @@ const getSearchWords = (location) => { * @param {Function} props.onFocusedGranuleChange - Callback to focus a granule. * @param {Function} props.onMetricsDataAccess - Callback to capture data access metrics. * @param {Function} props.onRemoveGranuleFromProjectCollection - Callback to remove a granule to the project. + * @param {Array} props.readableGranuleName - Array of Readable Granule Name strings. */ const GranuleResultsItem = forwardRef(({ collectionId, @@ -98,7 +81,8 @@ const GranuleResultsItem = forwardRef(({ onExcludeGranule, onFocusedGranuleChange, onMetricsDataAccess, - onRemoveGranuleFromProjectCollection + onRemoveGranuleFromProjectCollection, + readableGranuleName }, ref) => { const { thumbnailSize } = getApplicationConfig() const { @@ -242,7 +226,7 @@ const GranuleResultsItem = forwardRef(({ > @@ -383,7 +367,8 @@ GranuleResultsItem.propTypes = { onExcludeGranule: PropTypes.func.isRequired, onFocusedGranuleChange: PropTypes.func.isRequired, onMetricsDataAccess: PropTypes.func.isRequired, - onRemoveGranuleFromProjectCollection: PropTypes.func.isRequired + onRemoveGranuleFromProjectCollection: PropTypes.func.isRequired, + readableGranuleName: PropTypes.arrayOf(PropTypes.string).isRequired } export default GranuleResultsItem diff --git a/static/src/js/components/GranuleResults/GranuleResultsList.jsx b/static/src/js/components/GranuleResults/GranuleResultsList.jsx index 86c77bbe44..249b3a152e 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsList.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsList.jsx @@ -27,6 +27,7 @@ import './GranuleResultsList.scss' * @param {Function} props.onFocusedGranuleChange - Callback to change the focused granule. * @param {Function} props.onMetricsDataAccess - Callback to record data access metrics. * @param {Function} props.onRemoveGranuleFromProjectCollection - Callback to remove a granule to the project. + * @param {Array} props.readableGranuleName - The readableGranuleName filter value * @param {Function} props.setVisibleMiddleIndex - Callback to set the visible middle index. * @param {Number} props.visibleMiddleIndex - The current visible middle index. */ @@ -47,6 +48,7 @@ export const GranuleResultsList = ({ onFocusedGranuleChange, onMetricsDataAccess, onRemoveGranuleFromProjectCollection, + readableGranuleName, setVisibleMiddleIndex, visibleMiddleIndex }) => ( @@ -79,6 +81,7 @@ export const GranuleResultsList = ({ onFocusedGranuleChange={onFocusedGranuleChange} onMetricsDataAccess={onMetricsDataAccess} onRemoveGranuleFromProjectCollection={onRemoveGranuleFromProjectCollection} + readableGranuleName={readableGranuleName} setVisibleMiddleIndex={setVisibleMiddleIndex} visibleMiddleIndex={visibleMiddleIndex} width={width} @@ -111,6 +114,7 @@ GranuleResultsList.propTypes = { onFocusedGranuleChange: PropTypes.func.isRequired, onMetricsDataAccess: PropTypes.func.isRequired, onRemoveGranuleFromProjectCollection: PropTypes.func.isRequired, + readableGranuleName: PropTypes.arrayOf(PropTypes.string).isRequired, setVisibleMiddleIndex: PropTypes.func, visibleMiddleIndex: PropTypes.number } diff --git a/static/src/js/components/GranuleResults/GranuleResultsListBody.jsx b/static/src/js/components/GranuleResults/GranuleResultsListBody.jsx index 8f253eb7f3..f5cf842a2e 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsListBody.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsListBody.jsx @@ -95,6 +95,7 @@ export const GranuleResultsListBody = ({ onFocusedGranuleChange, onMetricsDataAccess, onRemoveGranuleFromProjectCollection, + readableGranuleName, setVisibleMiddleIndex, visibleMiddleIndex, width @@ -214,6 +215,7 @@ export const GranuleResultsListBody = ({ onFocusedGranuleChange, onMetricsDataAccess, onRemoveGranuleFromProjectCollection, + readableGranuleName, setRowHeight, windowHeight: height, windowWidth: width @@ -271,6 +273,7 @@ GranuleResultsListBody.propTypes = { onFocusedGranuleChange: PropTypes.func.isRequired, onMetricsDataAccess: PropTypes.func.isRequired, onRemoveGranuleFromProjectCollection: PropTypes.func.isRequired, + readableGranuleName: PropTypes.arrayOf(PropTypes.string).isRequired, setVisibleMiddleIndex: PropTypes.func, visibleMiddleIndex: PropTypes.number, width: PropTypes.number.isRequired diff --git a/static/src/js/components/GranuleResults/GranuleResultsListItem.jsx b/static/src/js/components/GranuleResults/GranuleResultsListItem.jsx index a65368b602..238781f074 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsListItem.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsListItem.jsx @@ -41,6 +41,7 @@ export const GranuleResultsListItem = memo(({ onFocusedGranuleChange, onMetricsDataAccess, onRemoveGranuleFromProjectCollection, + readableGranuleName, setRowHeight, windowWidth } = data @@ -109,6 +110,7 @@ export const GranuleResultsListItem = memo(({ onFocusedGranuleChange={onFocusedGranuleChange} onMetricsDataAccess={onMetricsDataAccess} onRemoveGranuleFromProjectCollection={onRemoveGranuleFromProjectCollection} + readableGranuleName={readableGranuleName} ref={element} /> @@ -133,6 +135,7 @@ GranuleResultsListItem.propTypes = { onFocusedGranuleChange: PropTypes.func, onMetricsDataAccess: PropTypes.func, onRemoveGranuleFromProjectCollection: PropTypes.func, + readableGranuleName: PropTypes.arrayOf(PropTypes.string).isRequired, setRowHeight: PropTypes.func, windowWidth: PropTypes.number }).isRequired, From 5ce33c3051cf556257fb093a2eb716f1983c23b7 Mon Sep 17 00:00:00 2001 From: Benjamin Poreh Date: Wed, 2 Oct 2024 14:30:06 -0400 Subject: [PATCH 05/26] EDSC-4238: converted tests into RTL from Enzyme. Broke up getSearchWords from GranuleResultsItem. --- .../GranuleResultsDataLinksButton.jsx | 17 +- .../GranuleResults/GranuleResultsItem.jsx | 31 +- .../__tests__/GranuleResultsItem.test.js | 431 ++++++++++++------ .../js/util/__tests__/getSearchWords.test.js | 46 ++ static/src/js/util/getSearchWords.js | 27 ++ 5 files changed, 370 insertions(+), 182 deletions(-) create mode 100644 static/src/js/util/__tests__/getSearchWords.test.js create mode 100644 static/src/js/util/getSearchWords.js diff --git a/static/src/js/components/GranuleResults/GranuleResultsDataLinksButton.jsx b/static/src/js/components/GranuleResults/GranuleResultsDataLinksButton.jsx index d6a93f9b2a..64010a7c34 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsDataLinksButton.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsDataLinksButton.jsx @@ -285,12 +285,17 @@ export const GranuleResultsDataLinksButton = ({ variant={buttonVariant} href={dataLinks[0].href} onClick={ - () => onMetricsDataAccess({ - type: 'single_granule_download', - collections: [{ - collectionId - }] - }) + (event) => { + onMetricsDataAccess({ + type: 'single_granule_download', + collections: [{ + collectionId + }] + }) + + event.preventDefault() + event.stopPropagation() + } } rel="noopener noreferrer" label="Download single granule data" diff --git a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx index 54049d44d9..f68c0d52cb 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx @@ -14,6 +14,7 @@ import { } from '@edsc/earthdata-react-icons/horizon-design-system/hds/ui' import { getApplicationConfig } from '../../../../../sharedUtils/config' +import { getSearchWords } from '../../util/getSearchWords' import murmurhash3 from '../../util/murmurhash3' import { locationPropType } from '../../util/propTypes/location' @@ -28,32 +29,6 @@ import PortalFeatureContainer from '../../containers/PortalFeatureContainer/Port import './GranuleResultsItem.scss' -/** - * Highlight substring if it's in being searched for in Granule Id(s) Filter - */ -const getSearchWords = (readableGranuleName) => { - const searchTerms = [] - - readableGranuleName.forEach((initialSearchTerm) => { - let splitStars = initialSearchTerm.split('*') - - // Remove the first and last stars if they are there - if (splitStars[0] === '') { - splitStars = splitStars.slice(1) - } - - if (splitStars[splitStars.length - 1] === '') { - splitStars = splitStars.slice(0, splitStars.length - 1) - } - - const searchTerm = splitStars.join('.+') - - searchTerms.push(RegExp(`(${searchTerm.replaceAll('?', '.')})`)) - }) - - return searchTerms -} - /** * Renders GranuleResultsItem. * @param {Object} props - The props passed into the component. @@ -132,6 +107,7 @@ const GranuleResultsItem = forwardRef(({ element = ( {element} @@ -217,6 +194,7 @@ const GranuleResultsItem = forwardRef(({ >
{ + const mockPortalFeatureContainer = jest.fn(({ children }) => ( + + {children} + + )) -Enzyme.configure({ adapter: new Adapter() }) + return mockPortalFeatureContainer +}) + +jest.mock('react-highlight-words', () => jest.fn( + ({ textToHighlight }) => {textToHighlight} +)) + +jest.mock('../../EDSCImage/EDSCImage', () => jest.fn( + ({ className }) =>
EDSC Image
+)) + +const setup = (type, overrideProps) => { + const user = userEvent.setup() -function setup(type, overrideProps) { const defaultProps = { browseUrl: undefined, collectionId: 'collectionId', @@ -28,6 +56,7 @@ function setup(type, overrideProps) { onFocusedGranuleChange: jest.fn(), onMetricsDataAccess: jest.fn(), onRemoveGranuleFromProjectCollection: jest.fn(), + readableGranuleName: [''], portal: { features: { authentication: true @@ -67,7 +96,9 @@ function setup(type, overrideProps) { props = { ...defaultProps, directDistributionInformation: { - region: 's3-region' + region: 's3-region', + s3CredentialsApiEndpoint: 'http://example.com/creds', + s3CredentialsApiDocumentationUrl: 'http://example.com/docs' }, granule: { id: 'granuleId', @@ -307,8 +338,7 @@ function setup(type, overrideProps) { } ], s3Links: [] - }, - ...overrideProps + } } } @@ -335,316 +365,417 @@ function setup(type, overrideProps) { } ], s3Links: [] - }, - ...overrideProps + } } } - const enzymeWrapper = shallow() + props = { + ...props, + ...overrideProps + } + + render( + , + { wrapper: MemoryRouter } + ) return { - enzymeWrapper, + user, props } } beforeEach(() => { jest.clearAllMocks() + + ReactDOM.createPortal = jest.fn((dropdown) => dropdown) }) describe('GranuleResultsItem component', () => { test('renders itself correctly', () => { - const { enzymeWrapper } = setup('cmr') + setup('cmr') - expect(enzymeWrapper.exists()).toEqual(true) - expect(enzymeWrapper.type()).toBe('div') + expect(screen.getByTestId('granule-results-item')).toBeInTheDocument() }) - test('renders the add button under PortalFeatureContainer', () => { - const { enzymeWrapper } = setup('cmr') - - const button = enzymeWrapper - .find(PortalFeatureContainer) - .find('.granule-results-item__button--add') - const portalFeatureContainer = button.parents(PortalFeatureContainer) + test('renders the add button under PortalFeatureContainer', async () => { + setup('cmr') - expect(button.exists()).toBeTruthy() - expect(portalFeatureContainer.props().authentication).toBeTruthy() + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Add granule' })).toBeInTheDocument() + }) }) describe('when passed a focused granule', () => { test('adds the correct classname', () => { - const { - enzymeWrapper - } = setup('focused-granule') + setup('focused-granule') + + expect(screen.getByTestId('granule-results-item')).toHaveClass('granule-results-item--active') + }) + }) + + describe('when passed a granule id filter', () => { + test('calls Highlighter', () => { + const spiedSearchWords = jest.spyOn(getSearchWords, 'getSearchWords') - expect(enzymeWrapper.props().className).toContain('granule-results-item--active') + setup('cmr', { readableGranuleName: ['title'] }) + + expect(Highlighter).toHaveBeenCalledTimes(1) + expect(Highlighter).toHaveBeenCalledWith( + { + highlightClassName: 'granule-results-item__highlighted-title', + searchWords: [/(title)/], + textToHighlight: 'Granule title' + }, + {} + ) + + expect(spiedSearchWords).toHaveBeenCalledTimes(1) }) }) describe('when passed a CMR granule', () => { test('renders the title', () => { - const { enzymeWrapper } = setup('cmr') + const { props } = setup('cmr') + const { granule } = props - expect(enzymeWrapper.find('.granule-results-item__title').text()).toEqual('Granule title') + const { title } = granule + + expect(screen.getByText(title)).toBeInTheDocument() }) + // Need to check the image src test('renders the image', () => { - const { enzymeWrapper } = setup('cmr') + const { props } = setup('cmr') + const { granule } = props + + const { title, granuleThumbnail } = granule - expect(enzymeWrapper.find('.granule-results-item__thumb-image').prop('src')).toEqual('/fake/path/image.jpg') + expect(EDSCImage).toHaveBeenCalledTimes(1) + expect(EDSCImage).toHaveBeenCalledWith( + expect.objectContaining( + { + alt: `Browse Image for ${title}`, + className: 'granule-results-item__thumb-image', + height: 85, + isBase64Image: true, + src: granuleThumbnail, + width: 85, + useSpinner: false + } + ), + {} + ) }) test('renders the start and end date', () => { - const { enzymeWrapper } = setup('cmr') + setup('cmr') - expect(enzymeWrapper.find('.granule-results-item__temporal--start').find('h5').text()).toEqual('Start') - expect(enzymeWrapper.find('.granule-results-item__temporal--start').find('p').text()).toEqual('2019-04-28 00:00:00') - expect(enzymeWrapper.find('.granule-results-item__temporal--end').find('h5').text()).toEqual('End') - expect(enzymeWrapper.find('.granule-results-item__temporal--end').find('p').text()).toEqual('2019-04-29 23:59:59') + expect(screen.getByRole('heading', { name: 'Start' })).toBeInTheDocument() + expect(screen.getByText('2019-04-28 00:00:00')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'End' })).toBeInTheDocument() + expect(screen.getByText('2019-04-29 23:59:59')).toBeInTheDocument() }) }) describe('when passed s3 links', () => { - test('passes the direct distribution information to the data links button', () => { - const { enzymeWrapper } = setup('cmr-s3') - const dataLinksButtonProps = enzymeWrapper.find(GranuleResultsDataLinksButton).props() + test('displaying correct s3 data', async () => { + const { user } = setup('cmr-s3') - expect(dataLinksButtonProps.directDistributionInformation).toEqual({ - region: 's3-region' + await act(async () => { + await user.click(screen.getByRole('button', { name: 'Download single granule data' })) }) - }) - test('passes the s3 links to the data links button', () => { - const { enzymeWrapper } = setup('cmr-s3') - const dataLinksButtonProps = enzymeWrapper.find(GranuleResultsDataLinksButton).props() + await act(async () => { + await user.click(screen.getByRole('tab', { name: 'AWS S3 Access' })) + }) - expect(dataLinksButtonProps.s3Links).toEqual([ - { - rel: 'http://linkrel/s3#', - title: 'linktitle', - href: 's3://linkhref' - } - ]) + expect(screen.getByRole('button', { name: 'Copy to clipboard' }).textContent).toEqual('s3-region') + expect(screen.getByRole('link', { name: 'Get AWS S3 Credentials' })).toHaveProperty('href', 'http://example.com/creds') + expect(screen.getByRole('link', { name: 'View Documentation' })).toHaveProperty('href', 'http://example.com/docs') + expect(screen.getByRole('button', { name: 'Copy AWS S3 path to clipboard' }).textContent).toEqual('linkhref') }) }) describe('when passed an OpenSearch granule', () => { test('renders the title', () => { - const { enzymeWrapper } = setup('opensearch') + setup('opensearch') - expect(enzymeWrapper.find('.granule-results-item__title').text()).toEqual('Granule title') + expect(screen.getByRole('heading', { name: 'Granule title' })).toBeInTheDocument() }) test('renders the image', () => { - const { enzymeWrapper } = setup('opensearch') + const { props } = setup('opensearch') + const { granule } = props - expect(enzymeWrapper.find('.granule-results-item__thumb-image').prop('src')).toEqual('/fake/path/image.jpg') + const { title, granuleThumbnail } = granule + + expect(EDSCImage).toHaveBeenCalledTimes(1) + expect(EDSCImage).toHaveBeenCalledWith( + expect.objectContaining( + { + alt: `Browse Image for ${title}`, + className: 'granule-results-item__thumb-image', + height: 85, + isBase64Image: true, + src: granuleThumbnail, + width: 85, + useSpinner: false + } + ), + {} + ) }) test('renders the start and end date', () => { - const { enzymeWrapper } = setup('opensearch') + setup('opensearch') - expect(enzymeWrapper.find('.granule-results-item__temporal--start').find('h5').text()).toEqual('Start') - expect(enzymeWrapper.find('.granule-results-item__temporal--start').find('p').text()).toEqual('2019-04-28 00:00:00') - expect(enzymeWrapper.find('.granule-results-item__temporal--end').find('h5').text()).toEqual('End') - expect(enzymeWrapper.find('.granule-results-item__temporal--end').find('p').text()).toEqual('2019-04-29 23:59:59') + expect(screen.getByRole('heading', { name: 'Start' })).toBeInTheDocument() + expect(screen.getByText('2019-04-28 00:00:00')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'End' })).toBeInTheDocument() + expect(screen.getByText('2019-04-29 23:59:59')).toBeInTheDocument() }) - test('disables the Add granule button', () => { - const { enzymeWrapper } = setup('opensearch') + test('Add granule button is disabled', () => { + setup('opensearch') - expect(enzymeWrapper.find('.granule-results-item__button--add').props().disabled).toBeTruthy() + expect(screen.getByRole('button', { name: 'Add granule' })).toBeDisabled() }) }) - describe('when clicking the remove button', () => { + describe('when clicking the Filter Granule button', () => { describe('with CMR granules', () => { - test('it removes the granule from results', () => { - const { enzymeWrapper, props } = setup('cmr') - const removeButton = enzymeWrapper.find(MoreActionsDropdownItem).at(1) + test('it removes the granule from results', async () => { + const { props, user } = setup('cmr') + + await act(async () => { + const moreActions = screen.getByRole('button', { name: 'More actions' }) + await user.click(moreActions) + }) + + await act(async () => { + await userEvent.click(screen.getByRole('button', { name: 'Filter granule' }), { pointerEventsCheck: 0 }) + }) - removeButton.simulate('click') expect(props.onExcludeGranule.mock.calls.length).toBe(1) expect(props.onExcludeGranule.mock.calls[0]).toEqual([{ collectionId: 'collectionId', granuleId: 'granuleId' }]) - - expect(removeButton.props().title).toEqual('Filter granule') }) }) describe('with OpenSearch granules', () => { - test('it excludes the granule from results with a hashed granule id', () => { - const { enzymeWrapper, props } = setup('opensearch') - const removeButton = enzymeWrapper.find(MoreActionsDropdownItem).at(1) + test('it excludes the granule from results with a hashed granule id', async () => { + const { props, user } = setup('opensearch') + + await act(async () => { + const moreActions = screen.getByRole('button', { name: 'More actions' }) + await user.click(moreActions) + }) + + await act(async () => { + await userEvent.click(screen.getByRole('button', { name: 'Filter granule' }), { pointerEventsCheck: 0 }) + }) - removeButton.simulate('click') expect(props.onExcludeGranule.mock.calls.length).toBe(1) expect(props.onExcludeGranule.mock.calls[0]).toEqual([{ collectionId: 'collectionId', granuleId: '170417722' }]) - - expect(removeButton.props().title).toEqual('Filter granule') }) }) }) describe('when no granules are in the project', () => { test('does not have an emphisized or deepmphisized class', () => { - const { enzymeWrapper } = setup('cmr') - expect(enzymeWrapper.props().className).not.toContain('granule-results-item--emphisized') - expect(enzymeWrapper.props().className).not.toContain('granule-results-item--deemphisized') + setup('cmr') + + const granuleResultsItem = screen.getByTestId('granule-results-item') + expect(granuleResultsItem.className).not.toContain('granule-results-item--emphisized') + expect(granuleResultsItem.className).not.toContain('granule-results-item--deemphisized') }) test('displays the add button', () => { - const { enzymeWrapper } = setup('cmr') - const addButton = enzymeWrapper.find(Button) - - expect(addButton.props().title).toContain('Add granule') + setup('cmr') + expect(screen.getByLabelText('Add granule')).toBeInTheDocument() }) }) describe('when displaying granules in the project', () => { describe('when displaying granule in the project', () => { test('displays the remove button', () => { - const { enzymeWrapper } = setup('override', { + setup('override', { isGranuleInProject: jest.fn(() => true), isCollectionInProject: true }) - const addButton = enzymeWrapper.find(Button) - expect(addButton.props().title).toContain('Remove granule') - expect(enzymeWrapper.props().className).toContain('granule-results-item--emphisized') + expect(screen.getByLabelText('Remove granule')).toBeInTheDocument() + + const granuleResultsItem = screen.getByTestId('granule-results-item') + expect(granuleResultsItem.className).toContain('granule-results-item--emphisized') }) }) describe('when displaying granule not in the project', () => { test('displays the add button', () => { - const { enzymeWrapper } = setup('override', { + setup('override', { isGranuleInProject: jest.fn(() => false), isCollectionInProject: true }) - const addButton = enzymeWrapper.find(Button) - expect(addButton.props().title).toContain('Add granule') - expect(enzymeWrapper.props().className).toContain('granule-results-item--deemphisized') + expect(screen.getByLabelText('Add granule')).toBeInTheDocument() + + const granuleResultsItem = screen.getByTestId('granule-results-item') + expect(granuleResultsItem.className).toContain('granule-results-item--deemphisized') }) }) }) describe('when clicking the add button', () => { - test('it adds the granule to the project', () => { - const { enzymeWrapper, props } = setup('cmr') - const addButton = enzymeWrapper.find(Button) + test('it adds the granule to the project', async () => { + const { props, user } = setup('cmr') - expect(addButton.props().title).toContain('Add granule') + expect(screen.getByLabelText('Add granule')).toBeInTheDocument() - const mockEvent = { - stopPropagation: jest.fn() - } + await user.click(screen.getByLabelText('Add granule')) - addButton.simulate('click', mockEvent) expect(props.onAddGranuleToProjectCollection.mock.calls.length).toBe(1) expect(props.onAddGranuleToProjectCollection.mock.calls[0]).toEqual([{ collectionId: 'collectionId', granuleId: 'granuleId' }]) - - expect(mockEvent.stopPropagation.mock.calls.length).toBe(1) - expect(mockEvent.stopPropagation.mock.calls[0]).toEqual([]) }) }) describe('when clicking the remove button', () => { - test('it removes the granule to the project', () => { - const { enzymeWrapper, props } = setup('override', { + test('it removes the granule to the project', async () => { + const { props, user } = setup('override', { isGranuleInProject: jest.fn(() => true) }) - const removeButton = enzymeWrapper.find(Button) - - expect(removeButton.props().title).toContain('Remove granule') + expect(screen.getByLabelText('Remove granule')).toBeInTheDocument() - const mockEvent = { - stopPropagation: jest.fn() - } - - removeButton.simulate('click', mockEvent) + await user.click(screen.getByLabelText('Remove granule')) expect(props.onRemoveGranuleFromProjectCollection.mock.calls.length).toBe(1) expect(props.onRemoveGranuleFromProjectCollection.mock.calls[0]).toEqual([{ collectionId: 'collectionId', granuleId: 'granuleId' }]) - - expect(mockEvent.stopPropagation.mock.calls.length).toBe(1) - expect(mockEvent.stopPropagation.mock.calls[0]).toEqual([]) }) }) describe('download button', () => { - test('is passed the metrics callback', () => { - const { enzymeWrapper, props } = setup('cmr') - const dataLinksButton = enzymeWrapper.find(GranuleResultsDataLinksButton) + test('is passed the metrics callback', async () => { + const { props, user } = setup('cmr') + + const { granule, onMetricsDataAccess, collectionId } = props - expect(dataLinksButton.props().onMetricsDataAccess).toEqual(props.onMetricsDataAccess) + const { dataLinks } = granule + + const dataLink = dataLinks[0] + + const { href } = dataLink + + const dataLinksButton = await screen.findByRole('button', { name: 'Download single granule data' }) + + expect(dataLinksButton.href).toContain(href) + + await act(async () => { + await user.click(dataLinksButton) + }) + + expect(onMetricsDataAccess).toHaveBeenCalledTimes(1) + expect(onMetricsDataAccess).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'single_granule_download', + collections: [{ + collectionId + }] + }) + ) }) }) describe('download button with no link', () => { test('disables the button', () => { - const { enzymeWrapper } = setup('cmr-no-download') - - const downloadButton = enzymeWrapper.find(GranuleResultsDataLinksButton) + setup('cmr-no-download') - expect(downloadButton.length).toEqual(0) + expect(screen.queryAllByLabelText('Download single granule data').length).toEqual(0) }) }) describe('without an granuleThumbnail', () => { test('does not render an granuleThumbnail', () => { - const { enzymeWrapper } = setup('no-thumb') + setup('no-thumb') - expect(enzymeWrapper.find('.granule-results-item__thumb').length).toEqual(0) + expect(screen.queryByLabelText('Link to granule')).not.toBeInTheDocument() + expect(screen.queryByLabelText('Granule thumbnail image')).not.toBeInTheDocument() }) test('does not add the modifier class name', () => { - const { enzymeWrapper } = setup('no-thumb') + setup('no-thumb') - expect(enzymeWrapper.props().className).not.toContain('granule-results-item--has-thumbnail') + expect(screen.getByTestId('granule-results-item').className).not.toContain('granule-results-item--has-thumbnail') }) }) describe('with a granuleThumbnail', () => { test('without a full size browse', () => { - const { enzymeWrapper } = setup('cmr') + const { props } = setup('cmr') - expect(enzymeWrapper.find('.granule-results-item__thumb').length).toEqual(1) - expect(enzymeWrapper.find('.granule-results-item__thumb').type()).toEqual('div') + const { granule } = props + + const { title, granuleThumbnail } = granule + + expect(EDSCImage).toHaveBeenCalledTimes(1) + expect(EDSCImage).toHaveBeenCalledWith( + expect.objectContaining( + { + alt: `Browse Image for ${title}`, + className: 'granule-results-item__thumb-image', + height: 85, + isBase64Image: true, + src: granuleThumbnail, + width: 85, + useSpinner: false + } + ), + {} + ) + + expect(screen.queryByLabelText('Link to granule')).not.toBeInTheDocument() }) test('with a full size browse', () => { - const { enzymeWrapper } = setup('with-browse') - expect(enzymeWrapper.find('.granule-results-item__thumb').length).toEqual(1) - expect(enzymeWrapper.find('.granule-results-item__thumb').type()).toEqual('a') + const { props } = setup('with-browse') + const { granule } = props + + const { browseUrl } = granule + + expect(screen.getByLabelText('Link to granule')).toBeInTheDocument() + expect(screen.getByLabelText('Link to granule').href).toEqual(browseUrl) }) test('adds the modifier class name', () => { - const { enzymeWrapper } = setup('with-browse') + setup('with-browse') - expect(enzymeWrapper.props().className).toContain('granule-results-item--has-thumbnail') + expect(screen.getByTestId('granule-results-item').className).toContain('granule-results-item--has-thumbnail') }) }) describe('granule info button', () => { - test('calls handleClickGranuleDetails on click', () => { - const { enzymeWrapper, props } = setup('cmr') + test('calls handleClickGranuleDetails on click', async () => { + const { props, user } = setup('cmr') - const infoButton = enzymeWrapper.find(LinkContainer).at(0) + await act(async () => { + const moreActions = screen.getByRole('button', { name: 'More actions' }) + await user.click(moreActions) + }) - infoButton.simulate('click') + await act(async () => { + await userEvent.click(screen.getByRole('button', { name: 'View details' }), { pointerEventsCheck: 0 }) + }) expect(props.onFocusedGranuleChange).toHaveBeenCalledTimes(1) expect(props.onFocusedGranuleChange).toHaveBeenCalledWith('granuleId') @@ -653,12 +784,12 @@ describe('GranuleResultsItem component', () => { describe('static coverage granules', () => { test('renders not provided for dates', () => { - const { enzymeWrapper } = setup('static-coverage') + setup('static-coverage') - expect(enzymeWrapper.find('.granule-results-item__temporal--start').find('h5').text()).toEqual('Start') - expect(enzymeWrapper.find('.granule-results-item__temporal--start').find('p').text()).toEqual('Not Provided') - expect(enzymeWrapper.find('.granule-results-item__temporal--end').find('h5').text()).toEqual('End') - expect(enzymeWrapper.find('.granule-results-item__temporal--end').find('p').text()).toEqual('Not Provided') + expect(screen.getByRole('heading', { name: 'Start' })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Start' }).nextSibling.textContent).toEqual('Not Provided') + expect(screen.getByRole('heading', { name: 'End' })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'End' }).nextSibling.textContent).toEqual('Not Provided') }) }) }) diff --git a/static/src/js/util/__tests__/getSearchWords.test.js b/static/src/js/util/__tests__/getSearchWords.test.js new file mode 100644 index 0000000000..a1ccde9bf1 --- /dev/null +++ b/static/src/js/util/__tests__/getSearchWords.test.js @@ -0,0 +1,46 @@ +import { getSearchWords } from '../getSearchWords' + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('getSearchWords', () => { + test('generates correct search words', () => { + const searchWords = getSearchWords(['*abc*']) + + expect(searchWords).toEqual([/(abc)/]) + }) + + test('generates correct search words with 1 ?', () => { + const searchWords = getSearchWords(['*a?c*']) + + expect(searchWords).toEqual([/(a.c)/]) + }) + + test('generates correct search words with multiple ?', () => { + const searchWords = getSearchWords(['*a?cde?*']) + + expect(searchWords).toEqual([/(a.cde.)/]) + }) + + test('generates correct search when no star in front', () => { + const searchWords = getSearchWords(['a?cde?*']) + + expect(searchWords).toEqual([/(a.cde.)/]) + }) + + test('generates correct search when no star in the end', () => { + const searchWords = getSearchWords(['*a?cde?']) + + expect(searchWords).toEqual([/(a.cde.)/]) + }) + + test('generates correct search words when there are multiple terms', () => { + const searchWords = getSearchWords(['*a?cde*', '*123?4*']) + + expect(searchWords).toEqual([ + /(a.cde)/, + /(123.4)/ + ]) + }) +}) diff --git a/static/src/js/util/getSearchWords.js b/static/src/js/util/getSearchWords.js new file mode 100644 index 0000000000..a4503ecd85 --- /dev/null +++ b/static/src/js/util/getSearchWords.js @@ -0,0 +1,27 @@ +/** + * @param {Array} readableGranuleName Array of strings to filter on the granule id. + * @returns {Array} Array of regex equivalent to the strings in readableGranuleName + * Highlight substring if it's in being searched for in Granule Id(s) Filter + */ +export const getSearchWords = (readableGranuleName) => { + const searchTerms = [] + + readableGranuleName.forEach((initialSearchTerm) => { + let splitStars = initialSearchTerm.split('*') + + // Remove the first and last stars if they are there + if (splitStars[0] === '') { + splitStars = splitStars.slice(1) + } + + if (splitStars[splitStars.length - 1] === '') { + splitStars = splitStars.slice(0, splitStars.length - 1) + } + + const searchTerm = splitStars.join('.+') + + searchTerms.push(RegExp(`(${searchTerm.replaceAll('?', '.')})`)) + }) + + return searchTerms +} From 35721f054766e5fa3918ef9fe23ec09eb8d12b76 Mon Sep 17 00:00:00 2001 From: Benjamin Poreh Date: Wed, 2 Oct 2024 14:30:44 -0400 Subject: [PATCH 06/26] removed import '@testing-library/jest-dom' --- .../GranuleResults/__tests__/GranuleResultsItem.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/static/src/js/components/GranuleResults/__tests__/GranuleResultsItem.test.js b/static/src/js/components/GranuleResults/__tests__/GranuleResultsItem.test.js index 6796fe79d7..ed18c9590a 100644 --- a/static/src/js/components/GranuleResults/__tests__/GranuleResultsItem.test.js +++ b/static/src/js/components/GranuleResults/__tests__/GranuleResultsItem.test.js @@ -9,8 +9,6 @@ import { } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import '@testing-library/jest-dom' - import { MemoryRouter } from 'react-router-dom' import Highlighter from 'react-highlight-words' From 74ea9f3d24976ee7e2adf5fce92cf2175d4c3913 Mon Sep 17 00:00:00 2001 From: Benjamin Poreh Date: Wed, 2 Oct 2024 16:51:36 -0400 Subject: [PATCH 07/26] EDSC-4238: fixed up readableGranuleName in GranuleResultsList.test.js --- .../GranuleResults/__tests__/GranuleResultsList.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/static/src/js/components/GranuleResults/__tests__/GranuleResultsList.test.js b/static/src/js/components/GranuleResults/__tests__/GranuleResultsList.test.js index 1882f5b424..3711de776a 100644 --- a/static/src/js/components/GranuleResults/__tests__/GranuleResultsList.test.js +++ b/static/src/js/components/GranuleResults/__tests__/GranuleResultsList.test.js @@ -64,6 +64,7 @@ function setup(type) { itemCount: 2, isItemLoaded: jest.fn(), loadMoreItems: jest.fn(), + readableGranuleName: [''], setVisibleMiddleIndex: jest.fn(), visibleMiddleIndex: 1 } From 6de922e601e9bd20ae5ce2d0e9769efff1ece820 Mon Sep 17 00:00:00 2001 From: Benjamin Poreh Date: Wed, 2 Oct 2024 17:08:44 -0400 Subject: [PATCH 08/26] EDSC-4238: Fixed up GranuleResultsDataLinksButton test --- .../GranuleResultsDataLinksButton.test.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/static/src/js/components/GranuleResults/__tests__/GranuleResultsDataLinksButton.test.js b/static/src/js/components/GranuleResults/__tests__/GranuleResultsDataLinksButton.test.js index 1b722fa65b..6ce25e56de 100644 --- a/static/src/js/components/GranuleResults/__tests__/GranuleResultsDataLinksButton.test.js +++ b/static/src/js/components/GranuleResults/__tests__/GranuleResultsDataLinksButton.test.js @@ -78,7 +78,13 @@ describe('GranuleResultsDataLinksButton component', () => { test('calls callback with the correct data on click', () => { const { enzymeWrapper, props } = setup() - enzymeWrapper.simulate('click') + const preventDefaultMock = jest.fn() + const stopPropagationMock = jest.fn() + + enzymeWrapper.simulate('click', { + preventDefault: preventDefaultMock, + stopPropagation: stopPropagationMock + }) expect(props.onMetricsDataAccess).toHaveBeenCalledTimes(1) expect(props.onMetricsDataAccess).toHaveBeenCalledWith({ @@ -87,6 +93,9 @@ describe('GranuleResultsDataLinksButton component', () => { ], type: 'single_granule_download' }) + + expect(preventDefaultMock).toHaveBeenCalledTimes(1) + expect(stopPropagationMock).toHaveBeenCalledTimes(1) }) test('renders the correct element', () => { @@ -187,7 +196,7 @@ describe('GranuleResultsDataLinksButton component', () => { const dataLinks = enzymeWrapper.find('.granule-results-data-links-button__dropdown-item') - dataLinks.at(0).simulate('click', { stopPropagation: () => {} }) + dataLinks.at(0).simulate('click', { stopPropagation: () => { } }) expect(props.onMetricsDataAccess).toHaveBeenCalledTimes(1) expect(props.onMetricsDataAccess).toHaveBeenCalledWith({ collections: [{ @@ -218,7 +227,7 @@ describe('GranuleResultsDataLinksButton component', () => { const dataLinks = enzymeWrapper.find('.granule-results-data-links-button__dropdown-item') - dataLinks.at(0).simulate('click', { stopPropagation: () => {} }) + dataLinks.at(0).simulate('click', { stopPropagation: () => { } }) expect(addToastMock.mock.calls.length).toBe(1) expect(addToastMock.mock.calls[0][0]).toEqual('Initiated download of file: linkhref') From 1fb98534c86ae6e48a42de755965c9849038e6b8 Mon Sep 17 00:00:00 2001 From: Benjamin Poreh Date: Fri, 4 Oct 2024 13:31:50 -0400 Subject: [PATCH 09/26] EDSC-4238: Addressed most of the comments in the PR --- .../GranuleResultsDataLinksButton.jsx | 1 - .../GranuleResults/GranuleResultsItem.jsx | 4 +--- .../GranuleResults/GranuleResultsItem.scss | 4 ++-- .../GranuleResults/GranuleResultsListBody.jsx | 1 + .../GranuleResultsDataLinksButton.test.js | 8 ++++---- .../__tests__/GranuleResultsItem.test.js | 14 +++++++------- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/static/src/js/components/GranuleResults/GranuleResultsDataLinksButton.jsx b/static/src/js/components/GranuleResults/GranuleResultsDataLinksButton.jsx index 64010a7c34..c957044c65 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsDataLinksButton.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsDataLinksButton.jsx @@ -293,7 +293,6 @@ export const GranuleResultsDataLinksButton = ({ }] }) - event.preventDefault() event.stopPropagation() } } diff --git a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx index f68c0d52cb..d19fe57a9d 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx @@ -3,7 +3,6 @@ import { PropTypes } from 'prop-types' import classNames from 'classnames' import { LinkContainer } from 'react-router-bootstrap' - import Highlighter from 'react-highlight-words' import { AlertInformation } from '@edsc/earthdata-react-icons/horizon-design-system/earthdata/ui' @@ -107,7 +106,6 @@ const GranuleResultsItem = forwardRef(({ element = (
{ test('calls callback with the correct data on click', () => { const { enzymeWrapper, props } = setup() - const preventDefaultMock = jest.fn() const stopPropagationMock = jest.fn() enzymeWrapper.simulate('click', { - preventDefault: preventDefaultMock, stopPropagation: stopPropagationMock }) @@ -94,7 +92,6 @@ describe('GranuleResultsDataLinksButton component', () => { type: 'single_granule_download' }) - expect(preventDefaultMock).toHaveBeenCalledTimes(1) expect(stopPropagationMock).toHaveBeenCalledTimes(1) }) @@ -207,6 +204,7 @@ describe('GranuleResultsDataLinksButton component', () => { }) test('displays a success toast', () => { + const stopPropagationMock = jest.fn() // Mocks createPortal method of ReactDOM (https://stackoverflow.com/a/60953708/8116576) ReactDOM.createPortal = jest.fn((dropdown) => dropdown) const addToastMock = jest.spyOn(addToast, 'addToast') @@ -227,7 +225,9 @@ describe('GranuleResultsDataLinksButton component', () => { const dataLinks = enzymeWrapper.find('.granule-results-data-links-button__dropdown-item') - dataLinks.at(0).simulate('click', { stopPropagation: () => { } }) + dataLinks.at(0).simulate('click', { stopPropagation: stopPropagationMock }) + + expect(stopPropagationMock).toHaveBeenCalledTimes(1) expect(addToastMock.mock.calls.length).toBe(1) expect(addToastMock.mock.calls[0][0]).toEqual('Initiated download of file: linkhref') diff --git a/static/src/js/components/GranuleResults/__tests__/GranuleResultsItem.test.js b/static/src/js/components/GranuleResults/__tests__/GranuleResultsItem.test.js index ed18c9590a..1ddefd604b 100644 --- a/static/src/js/components/GranuleResults/__tests__/GranuleResultsItem.test.js +++ b/static/src/js/components/GranuleResults/__tests__/GranuleResultsItem.test.js @@ -393,7 +393,7 @@ describe('GranuleResultsItem component', () => { test('renders itself correctly', () => { setup('cmr') - expect(screen.getByTestId('granule-results-item')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Focus granule on map' })).toBeInTheDocument() }) test('renders the add button under PortalFeatureContainer', async () => { @@ -408,7 +408,7 @@ describe('GranuleResultsItem component', () => { test('adds the correct classname', () => { setup('focused-granule') - expect(screen.getByTestId('granule-results-item')).toHaveClass('granule-results-item--active') + expect(screen.getByRole('button', { name: 'Focus granule on map' })).toHaveClass('granule-results-item--active') }) }) @@ -589,7 +589,7 @@ describe('GranuleResultsItem component', () => { test('does not have an emphisized or deepmphisized class', () => { setup('cmr') - const granuleResultsItem = screen.getByTestId('granule-results-item') + const granuleResultsItem = screen.getByRole('button', { name: 'Focus granule on map' }) expect(granuleResultsItem.className).not.toContain('granule-results-item--emphisized') expect(granuleResultsItem.className).not.toContain('granule-results-item--deemphisized') }) @@ -610,7 +610,7 @@ describe('GranuleResultsItem component', () => { expect(screen.getByLabelText('Remove granule')).toBeInTheDocument() - const granuleResultsItem = screen.getByTestId('granule-results-item') + const granuleResultsItem = screen.getByRole('button', { name: 'Focus granule on map' }) expect(granuleResultsItem.className).toContain('granule-results-item--emphisized') }) }) @@ -624,7 +624,7 @@ describe('GranuleResultsItem component', () => { expect(screen.getByLabelText('Add granule')).toBeInTheDocument() - const granuleResultsItem = screen.getByTestId('granule-results-item') + const granuleResultsItem = screen.getByRole('button', { name: 'Focus granule on map' }) expect(granuleResultsItem.className).toContain('granule-results-item--deemphisized') }) }) @@ -714,7 +714,7 @@ describe('GranuleResultsItem component', () => { test('does not add the modifier class name', () => { setup('no-thumb') - expect(screen.getByTestId('granule-results-item').className).not.toContain('granule-results-item--has-thumbnail') + expect(screen.getByRole('button', { name: 'Focus granule on map' }).className).not.toContain('granule-results-item--has-thumbnail') }) }) @@ -758,7 +758,7 @@ describe('GranuleResultsItem component', () => { test('adds the modifier class name', () => { setup('with-browse') - expect(screen.getByTestId('granule-results-item').className).toContain('granule-results-item--has-thumbnail') + expect(screen.getByRole('button', { name: 'Focus granule on map' }).className).toContain('granule-results-item--has-thumbnail') }) }) From 7651751bc1228d10f3857524124205c6db1ddde8 Mon Sep 17 00:00:00 2001 From: Benjamin Poreh Date: Mon, 7 Oct 2024 10:03:10 -0400 Subject: [PATCH 10/26] EDSC-4238: fixed up some more tests --- .../js/components/GranuleResults/GranuleResultsItem.jsx | 1 - .../GranuleResults/__tests__/GranuleResultsItem.test.js | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx index d19fe57a9d..57dbe3b3f8 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx @@ -122,7 +122,6 @@ const GranuleResultsItem = forwardRef(({ href={browseUrl} title="View image" target="_blank" - aria-label="Link to granule" rel="noopener noreferrer" > {element} diff --git a/static/src/js/components/GranuleResults/__tests__/GranuleResultsItem.test.js b/static/src/js/components/GranuleResults/__tests__/GranuleResultsItem.test.js index 1ddefd604b..43f1400c19 100644 --- a/static/src/js/components/GranuleResults/__tests__/GranuleResultsItem.test.js +++ b/static/src/js/components/GranuleResults/__tests__/GranuleResultsItem.test.js @@ -408,7 +408,7 @@ describe('GranuleResultsItem component', () => { test('adds the correct classname', () => { setup('focused-granule') - expect(screen.getByRole('button', { name: 'Focus granule on map' })).toHaveClass('granule-results-item--active') + expect(screen.getByRole('button', { name: 'Unfocus granule on map' })).toHaveClass('granule-results-item--active') }) }) @@ -707,7 +707,7 @@ describe('GranuleResultsItem component', () => { test('does not render an granuleThumbnail', () => { setup('no-thumb') - expect(screen.queryByLabelText('Link to granule')).not.toBeInTheDocument() + expect(screen.queryByRole('link', { name: 'View image' })).not.toBeInTheDocument() expect(screen.queryByLabelText('Granule thumbnail image')).not.toBeInTheDocument() }) @@ -751,8 +751,8 @@ describe('GranuleResultsItem component', () => { const { browseUrl } = granule - expect(screen.getByLabelText('Link to granule')).toBeInTheDocument() - expect(screen.getByLabelText('Link to granule').href).toEqual(browseUrl) + expect(screen.getByRole('link', { name: 'View image' })).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'View image' })).toHaveAttribute('href', browseUrl) }) test('adds the modifier class name', () => { From 41528d67e0555a11454a61c475aaec3f9594c8a9 Mon Sep 17 00:00:00 2001 From: Benjamin Poreh Date: Mon, 7 Oct 2024 10:10:22 -0400 Subject: [PATCH 11/26] EDSC-4238: Addressed some more comments --- .../GranuleResultsDataLinksButton.test.js | 7 ++++++- static/src/js/util/getSearchWords.js | 17 +++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/static/src/js/components/GranuleResults/__tests__/GranuleResultsDataLinksButton.test.js b/static/src/js/components/GranuleResults/__tests__/GranuleResultsDataLinksButton.test.js index dd96d9ccb1..71e14b4f30 100644 --- a/static/src/js/components/GranuleResults/__tests__/GranuleResultsDataLinksButton.test.js +++ b/static/src/js/components/GranuleResults/__tests__/GranuleResultsDataLinksButton.test.js @@ -174,6 +174,7 @@ describe('GranuleResultsDataLinksButton component', () => { }) test('calls the metrics event', () => { + const stopPropagationMock = jest.fn() // Mocks createPortal method of ReactDOM (https://stackoverflow.com/a/60953708/8116576) ReactDOM.createPortal = jest.fn((dropdown) => dropdown) @@ -193,7 +194,11 @@ describe('GranuleResultsDataLinksButton component', () => { const dataLinks = enzymeWrapper.find('.granule-results-data-links-button__dropdown-item') - dataLinks.at(0).simulate('click', { stopPropagation: () => { } }) + dataLinks.at(0).simulate('click', { + stopPropagation: stopPropagationMock + }) + + expect(stopPropagationMock).toHaveBeenCalledTimes(1) expect(props.onMetricsDataAccess).toHaveBeenCalledTimes(1) expect(props.onMetricsDataAccess).toHaveBeenCalledWith({ collections: [{ diff --git a/static/src/js/util/getSearchWords.js b/static/src/js/util/getSearchWords.js index a4503ecd85..fea1be5907 100644 --- a/static/src/js/util/getSearchWords.js +++ b/static/src/js/util/getSearchWords.js @@ -1,12 +1,10 @@ /** * @param {Array} readableGranuleName Array of strings to filter on the granule id. * @returns {Array} Array of regex equivalent to the strings in readableGranuleName - * Highlight substring if it's in being searched for in Granule Id(s) Filter + * Convert our search terms into their Regex equivalent */ -export const getSearchWords = (readableGranuleName) => { - const searchTerms = [] - - readableGranuleName.forEach((initialSearchTerm) => { +export const getSearchWords = (readableGranuleName) => readableGranuleName.map( + (initialSearchTerm) => { let splitStars = initialSearchTerm.split('*') // Remove the first and last stars if they are there @@ -20,8 +18,7 @@ export const getSearchWords = (readableGranuleName) => { const searchTerm = splitStars.join('.+') - searchTerms.push(RegExp(`(${searchTerm.replaceAll('?', '.')})`)) - }) - - return searchTerms -} + // Replacing all the ? (single letter placeholders) with the equivalent regex + return (RegExp(`(${searchTerm.replaceAll('?', '.')})`)) + } +) From 3b117b89b389e8110953e393eeec4d9606b1e1ee Mon Sep 17 00:00:00 2001 From: Benjamin Poreh Date: Mon, 7 Oct 2024 10:56:52 -0400 Subject: [PATCH 12/26] EDSC-4238: a bit more changes to grab the title buttons using the granule name --- .../GranuleResults/GranuleResultsItem.jsx | 3 +-- .../__tests__/GranuleResultsItem.test.js | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx index 57dbe3b3f8..0089df171d 100644 --- a/static/src/js/components/GranuleResults/GranuleResultsItem.jsx +++ b/static/src/js/components/GranuleResults/GranuleResultsItem.jsx @@ -3,6 +3,7 @@ import { PropTypes } from 'prop-types' import classNames from 'classnames' import { LinkContainer } from 'react-router-bootstrap' + import Highlighter from 'react-highlight-words' import { AlertInformation } from '@edsc/earthdata-react-icons/horizon-design-system/earthdata/ui' @@ -186,7 +187,6 @@ const GranuleResultsItem = forwardRef(({ ref={ref} role="button" tabIndex={0} - aria-label={itemTitle.title} // eslint-disable-next-line react/jsx-props-no-spreading {...itemTitle} > @@ -250,7 +250,6 @@ const GranuleResultsItem = forwardRef(({