From f3bc3a90ab3f354b9c07125160f9ffcaebf70ee9 Mon Sep 17 00:00:00 2001 From: Liz Wendling <170662489+GovCIOLiz@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:42:08 -0800 Subject: [PATCH] GI CT Search by program (#34150) * Wip * Program search required fields * Search by program components * Testing * Testing * Testing, require distance field * Testing and focusLocationInput on location modal close * Distance dropdown validation message, testing * Toggle --- src/applications/gi/actions/index.js | 26 ++-- .../gi/tests/actions/index.unit.spec.jsx | 1 + .../updated-gi/components/SearchByProgram.jsx | 75 ----------- .../school-and-employers/UseMyLocation.jsx | 26 ++++ .../UseMyLocationModal.jsx | 30 +++++ .../containers/SchoolsAndEmployers.jsx | 40 +++--- .../SearchByName.jsx | 0 .../updated-gi/containers/SearchByProgram.jsx | 125 ++++++++++++++++++ .../components/AboutThisTool.unit.spec.jsx | 11 ++ .../components/UseMyLocation.unit.spec.jsx | 27 ++++ .../UseMyLocationModal.unit.spec.jsx | 64 +++++++++ .../SchoolsAndEmployers.unit.spec.jsx | 10 +- .../containers/SearchByProgram.unit.spec.jsx | 105 +++++++++++++++ .../e2e/updated_gi_homepage.cypress.spec.js | 6 + 14 files changed, 444 insertions(+), 102 deletions(-) delete mode 100644 src/applications/gi/updated-gi/components/SearchByProgram.jsx create mode 100644 src/applications/gi/updated-gi/components/school-and-employers/UseMyLocation.jsx create mode 100644 src/applications/gi/updated-gi/components/school-and-employers/UseMyLocationModal.jsx rename src/applications/gi/updated-gi/{components => containers}/SearchByName.jsx (100%) create mode 100644 src/applications/gi/updated-gi/containers/SearchByProgram.jsx create mode 100644 src/applications/gi/updated-gi/tests/components/AboutThisTool.unit.spec.jsx create mode 100644 src/applications/gi/updated-gi/tests/components/UseMyLocation.unit.spec.jsx create mode 100644 src/applications/gi/updated-gi/tests/components/UseMyLocationModal.unit.spec.jsx create mode 100644 src/applications/gi/updated-gi/tests/containers/SearchByProgram.unit.spec.jsx diff --git a/src/applications/gi/actions/index.js b/src/applications/gi/actions/index.js index f7e46dec4a8e..d6ec51cdd4fa 100644 --- a/src/applications/gi/actions/index.js +++ b/src/applications/gi/actions/index.js @@ -507,15 +507,23 @@ export function fetchSearchByLocationCoords( distance, filters, version, + description, ) { const [longitude, latitude] = coordinates; - - const params = { - latitude, - longitude, - distance, - ...rubyifyKeys(buildSearchFilters(filters)), - }; + // If description - search by program, else search by location w/ filters + const params = description + ? { + latitude, + longitude, + distance, + description, + } + : { + latitude, + longitude, + distance, + ...rubyifyKeys(filters && buildSearchFilters(filters)), + }; if (version) { params.version = version; } @@ -523,7 +531,7 @@ export function fetchSearchByLocationCoords( return dispatch => { dispatch({ type: SEARCH_STARTED, - payload: { location, latitude, longitude, distance }, + payload: { location, latitude, longitude, distance, description }, }); return fetch(url, api.settings) @@ -563,6 +571,7 @@ export function fetchSearchByLocationResults( distance, filters, version, + description, ) { // Prevent empty search request to Mapbox, which would result in error, and // clear results list to respond with message of no facilities found. @@ -593,6 +602,7 @@ export function fetchSearchByLocationResults( distance, filters, version, + description, ), ); }) diff --git a/src/applications/gi/tests/actions/index.unit.spec.jsx b/src/applications/gi/tests/actions/index.unit.spec.jsx index bf0fdca7c268..e9dc1355b2ab 100644 --- a/src/applications/gi/tests/actions/index.unit.spec.jsx +++ b/src/applications/gi/tests/actions/index.unit.spec.jsx @@ -549,6 +549,7 @@ describe('actionCreators', () => { 10, { filter1: 'value1', excludedSchoolTypes: 'value2' }, 'version', + null, )(mockDispatch) .then(() => { expect( diff --git a/src/applications/gi/updated-gi/components/SearchByProgram.jsx b/src/applications/gi/updated-gi/components/SearchByProgram.jsx deleted file mode 100644 index 138497071fd8..000000000000 --- a/src/applications/gi/updated-gi/components/SearchByProgram.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useState } from 'react'; -import { - VaButton, - VaSelect, - VaTextInput, -} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; - -const SearchByProgram = () => { - const distanceDropdownOptions = [ - { value: '5', label: 'within 5 miles' }, - { value: '15', label: 'within 15 miles' }, - { value: '25', label: 'within 25 miles' }, - { value: '50', label: 'within 50 miles' }, - { value: '75', label: 'within 75 miles' }, - ]; - const [distance, setDistance] = useState('25'); - - const useLocation = e => { - e.preventDefault(); - }; - - const onSelectChange = e => { - setDistance(e.target.value); - }; - - const search = () => {}; - - return ( -
- - -
- {/* eslint-disable-next-line @department-of-veterans-affairs/prefer-button-component */} - - - {distanceDropdownOptions.map(option => ( - - ))} - -
- -
- ); -}; - -export default SearchByProgram; diff --git a/src/applications/gi/updated-gi/components/school-and-employers/UseMyLocation.jsx b/src/applications/gi/updated-gi/components/school-and-employers/UseMyLocation.jsx new file mode 100644 index 000000000000..07972e0a52ac --- /dev/null +++ b/src/applications/gi/updated-gi/components/school-and-employers/UseMyLocation.jsx @@ -0,0 +1,26 @@ +import React from 'react'; + +export const UseMyLocation = ({ geolocationInProgress, handleLocateUser }) => { + return ( + <> + {geolocationInProgress ? ( +
+
+ ) : ( + <> + {/* eslint-disable-next-line @department-of-veterans-affairs/prefer-button-component */} + + + )} + + ); +}; diff --git a/src/applications/gi/updated-gi/components/school-and-employers/UseMyLocationModal.jsx b/src/applications/gi/updated-gi/components/school-and-employers/UseMyLocationModal.jsx new file mode 100644 index 000000000000..0489ce2c6d11 --- /dev/null +++ b/src/applications/gi/updated-gi/components/school-and-employers/UseMyLocationModal.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { VaModal } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { clearGeocodeError } from '../../../actions'; + +export const UseMyLocationModal = ({ geocodeError, focusLocationInput }) => { + const dispatch = useDispatch(); + + return ( + { + focusLocationInput(); + dispatch(clearGeocodeError()); + }} + status="warning" + visible={geocodeError > 0} + > +

+ {geocodeError === 1 + ? 'Please enable location sharing in your browser to use this feature.' + : 'Sorry, something went wrong when trying to find your location. Please make sure location sharing is enabled and try again.'} +

+
+ ); +}; diff --git a/src/applications/gi/updated-gi/containers/SchoolsAndEmployers.jsx b/src/applications/gi/updated-gi/containers/SchoolsAndEmployers.jsx index 707e8e10dcb4..621b18cc58d1 100644 --- a/src/applications/gi/updated-gi/containers/SchoolsAndEmployers.jsx +++ b/src/applications/gi/updated-gi/containers/SchoolsAndEmployers.jsx @@ -1,14 +1,14 @@ import React, { useState } from 'react'; import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; -import SearchByName from '../components/SearchByName'; -import SearchByProgram from '../components/SearchByProgram'; +import SearchByName from './SearchByName'; +import SearchByProgram from './SearchByProgram'; const SchoolAndEmployers = () => { const [currentTab, setCurrentTab] = useState(0); const tabPanelClassList = 'vads-u-border-bottom--1px vads-u-border-left--1px vads-u-border-right--1px vads-u-border-color--primary medium-screen:vads-u-padding--4 mobile:vads-u-padding--2'; const baseTabClassList = - 'vads-l-col vads-u-display--flex vads-u-justify-content--center vads-u-align-items--center vads-u-margin-bottom--0 vads-u-text-align--center vads-u-border-top--5px vads-u-border-left--1px vads-u-border-right--1px'; + 'vads-u-font-family--serif vads-u-font-size--h3 vads-l-col vads-u-display--flex vads-u-justify-content--center vads-u-align-items--center vads-u-margin-bottom--0 vads-u-text-align--center vads-u-border-top--5px vads-u-border-left--1px vads-u-border-right--1px'; const inactiveTabClassList = `${baseTabClassList} vads-u-background-color--base-lightest vads-u-border-color--base-lightest vads-u-border-bottom--1px`; const activeTabClassList = `${baseTabClassList} vads-u-border-color--primary`; const inactiveTabText = 'vads-u-color--gray-dark vads-u-margin--0'; @@ -27,30 +27,36 @@ const SchoolAndEmployers = () => { style={{ listStyle: 'none', cursor: 'pointer' }} > -

- Search by name -

+ + + Search by name + +
-

- Search by program -

+ + + Search by program + +
diff --git a/src/applications/gi/updated-gi/components/SearchByName.jsx b/src/applications/gi/updated-gi/containers/SearchByName.jsx similarity index 100% rename from src/applications/gi/updated-gi/components/SearchByName.jsx rename to src/applications/gi/updated-gi/containers/SearchByName.jsx diff --git a/src/applications/gi/updated-gi/containers/SearchByProgram.jsx b/src/applications/gi/updated-gi/containers/SearchByProgram.jsx new file mode 100644 index 000000000000..2145f36e798f --- /dev/null +++ b/src/applications/gi/updated-gi/containers/SearchByProgram.jsx @@ -0,0 +1,125 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import recordEvent from 'platform/monitoring/record-event'; +import { + VaButton, + VaSelect, + VaTextInput, +} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { geolocateUser, fetchSearchByLocationResults } from '../../actions'; +import { UseMyLocation } from '../components/school-and-employers/UseMyLocation'; +import { UseMyLocationModal } from '../components/school-and-employers/UseMyLocationModal'; + +const SearchByProgram = () => { + const locationRef = useRef(null); + const distanceDropdownOptions = [ + { value: '5', label: 'within 5 miles' }, + { value: '15', label: 'within 15 miles' }, + { value: '25', label: 'within 25 miles' }, + { value: '50', label: 'within 50 miles' }, + { value: '75', label: 'within 75 miles' }, + ]; + const dispatch = useDispatch(); + const search = useSelector(state => state.search); + const [distance, setDistance] = useState(search.query.distance); + const [location, setLocation] = useState(search.query.location); + const [programName, setProgramName] = useState(null); + const [searchDirty, setSearchDirty] = useState(false); + + const focusLocationInput = () => { + locationRef?.current?.shadowRoot?.querySelector('input').focus(); + }; + + const handleLocateUser = e => { + e.preventDefault(); + recordEvent({ + event: 'map-use-my-location', + }); + dispatch(geolocateUser()); + }; + + const handleSearch = () => { + if (!searchDirty) { + setSearchDirty(true); + return; + } + const description = programName; + dispatch( + fetchSearchByLocationResults(location, distance, null, null, description), + ); + // Show program results... + }; + + useEffect( + () => { + const { searchString } = search.query.streetAddress; + if (searchString) { + setLocation(searchString); + focusLocationInput(); + } + }, + [search], + ); + + return ( +
+ + setProgramName(e.target.value)} + /> + setLocation(e.target.value)} + /> +
+ + setDistance(e.target.value)} + value={distance} + required + error={searchDirty && !distance ? 'Please select a distance' : null} + > + {distanceDropdownOptions.map(option => ( + + ))} + +
+ +
+ ); +}; + +export default SearchByProgram; diff --git a/src/applications/gi/updated-gi/tests/components/AboutThisTool.unit.spec.jsx b/src/applications/gi/updated-gi/tests/components/AboutThisTool.unit.spec.jsx new file mode 100644 index 000000000000..1da76eda73e4 --- /dev/null +++ b/src/applications/gi/updated-gi/tests/components/AboutThisTool.unit.spec.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { expect } from 'chai'; +import { render } from '@testing-library/react'; +import { AboutThisTool } from '../../components/AboutThisTool'; + +describe('About this tool', () => { + it('Should render two links', () => { + const { container } = render(); + expect(container.querySelectorAll('a').length).to.equal(2); + }); +}); diff --git a/src/applications/gi/updated-gi/tests/components/UseMyLocation.unit.spec.jsx b/src/applications/gi/updated-gi/tests/components/UseMyLocation.unit.spec.jsx new file mode 100644 index 000000000000..680decb3f4fd --- /dev/null +++ b/src/applications/gi/updated-gi/tests/components/UseMyLocation.unit.spec.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { expect } from 'chai'; +import { render } from '@testing-library/react'; +import createCommonStore from '@department-of-veterans-affairs/platform-startup/store'; +import { UseMyLocation } from '../../components/school-and-employers/UseMyLocation'; + +const defaultStore = createCommonStore(); + +describe('Use my location', () => { + it('Should show finding your location when in progress', () => { + const { getByText } = render( + + + , + ); + expect(getByText('Finding your location...')).to.exist; + }); + it('Should show finding your location when in progress', () => { + const { getByText } = render( + + + , + ); + expect(getByText('Use my location')).to.exist; + }); +}); diff --git a/src/applications/gi/updated-gi/tests/components/UseMyLocationModal.unit.spec.jsx b/src/applications/gi/updated-gi/tests/components/UseMyLocationModal.unit.spec.jsx new file mode 100644 index 000000000000..1f12a5157111 --- /dev/null +++ b/src/applications/gi/updated-gi/tests/components/UseMyLocationModal.unit.spec.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import thunk from 'redux-thunk'; +import { Provider } from 'react-redux'; +import { expect } from 'chai'; +import { render, waitFor } from '@testing-library/react'; +import { $ } from 'platform/forms-system/src/js/utilities/ui'; +import configureStore from 'redux-mock-store'; +import createCommonStore from '@department-of-veterans-affairs/platform-startup/store'; +import SearchByProgram from '../../containers/SearchByProgram'; +import { UseMyLocationModal } from '../../components/school-and-employers/UseMyLocationModal'; + +const defaultStore = createCommonStore(); + +describe('Use my location modal', () => { + it('Renders enable location modal', () => { + const { getByText } = render( + + + , + ); + expect( + getByText( + 'Please enable location sharing in your browser to use this feature.', + ), + ).to.exist; + }); + it('Renders couldnt locate modal', () => { + const { getByText } = render( + + + , + ); + expect( + getByText( + 'Sorry, something went wrong when trying to find your location. Please make sure location sharing is enabled and try again.', + ), + ).to.exist; + }); + it('Should close', async () => { + const middlewares = [thunk]; + const mockStore = configureStore(middlewares); + const store = mockStore({ + search: { + query: { + distance: '', + location: '', + streetAddress: { searchString: '' }, + }, + }, + }); + const { container } = render( + + + + + , + ); + const event = new CustomEvent('closeEvent'); + await $('va-modal', container).__events.closeEvent(event); + waitFor(() => { + expect($('va-modal[visible="false"]', container)).to.exist; + }); + }); +}); diff --git a/src/applications/gi/updated-gi/tests/containers/SchoolsAndEmployers.unit.spec.jsx b/src/applications/gi/updated-gi/tests/containers/SchoolsAndEmployers.unit.spec.jsx index a53bbe59d7db..e34f39f02321 100644 --- a/src/applications/gi/updated-gi/tests/containers/SchoolsAndEmployers.unit.spec.jsx +++ b/src/applications/gi/updated-gi/tests/containers/SchoolsAndEmployers.unit.spec.jsx @@ -5,7 +5,13 @@ import SchoolsAndEmployers from '../../containers/SchoolsAndEmployers'; describe('Schools and employers', () => { it('Renders without crashing', () => { - const { container } = render(); - expect(container).to.exist; + const { getByText } = render(); + expect(getByText('Schools and employers')).to.exist; + }); + + it('Renders with Search by name as default tab', () => { + const { getByRole } = render(); + const nameTab = getByRole('tab', { name: 'Search by name' }); + expect(nameTab.getAttribute('aria-selected')).to.equal('true'); }); }); diff --git a/src/applications/gi/updated-gi/tests/containers/SearchByProgram.unit.spec.jsx b/src/applications/gi/updated-gi/tests/containers/SearchByProgram.unit.spec.jsx new file mode 100644 index 000000000000..34d2f32dedb8 --- /dev/null +++ b/src/applications/gi/updated-gi/tests/containers/SearchByProgram.unit.spec.jsx @@ -0,0 +1,105 @@ +import React from 'react'; +import thunk from 'redux-thunk'; +import { Provider } from 'react-redux'; +import { expect } from 'chai'; +import { render } from '@testing-library/react'; +import configureStore from 'redux-mock-store'; +import { $$ } from 'platform/forms-system/src/js/utilities/ui'; +import userEvent from '@testing-library/user-event'; +import SearchByProgram from '../../containers/SearchByProgram'; + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +describe('Search by program', () => { + let store; + + it('Renders inputs and search button', () => { + store = mockStore({ + search: { + query: { + distance: '25', + location: '', + streetAddress: { searchString: '' }, + }, + }, + }); + const { container } = render( + + + , + ); + expect($$('va-text-input', container).length).to.equal(2); + expect($$('va-select', container).length).to.equal(1); + expect($$('va-button', container).length).to.equal(1); + }); + + it('Shows required input errors', () => { + store = mockStore({ + search: { + query: { + distance: '', + location: '', + streetAddress: { searchString: '' }, + }, + }, + }); + const { container } = render( + + + , + ); + userEvent.click(container.getElementsByTagName('va-button')[0]); + expect(container.querySelectorAll('va-text-input[error]').length).to.equal( + 2, + ); + expect(container.querySelectorAll('va-select[error]').length).to.equal(1); + }); + + it('Shows failed attempt to locate user - user not sharing location', () => { + store = mockStore({ + search: { + query: { + distance: '25', + location: '', + streetAddress: { searchString: '' }, + }, + }, + }); + delete global.navigator.geolocation; + const { getByText } = render( + + + , + ); + userEvent.click(getByText('Use my location')); + expect( + getByText( + 'Sorry, something went wrong when trying to find your location. Please make sure location sharing is enabled and try again.', + ), + ).to.exist; + }); + + it('Fills user location if location found', () => { + store = mockStore({ + search: { + query: { + distance: '25', + location: '', + streetAddress: { + searchString: '1313 Disneyland Dr Anaheim, CA 92802', + }, + }, + }, + }); + const { container } = render( + + + , + ); + userEvent.click(container.getElementsByTagName('va-button')[0]); + expect(container.querySelectorAll('va-text-input[error]').length).to.equal( + 1, + ); + }); +}); diff --git a/src/applications/gi/updated-gi/tests/e2e/updated_gi_homepage.cypress.spec.js b/src/applications/gi/updated-gi/tests/e2e/updated_gi_homepage.cypress.spec.js index 25390569cdf9..411a658e31ab 100644 --- a/src/applications/gi/updated-gi/tests/e2e/updated_gi_homepage.cypress.spec.js +++ b/src/applications/gi/updated-gi/tests/e2e/updated_gi_homepage.cypress.spec.js @@ -20,4 +20,10 @@ describe('go bill CT new homepage', () => { 'Discover how your GI Bill benefits can support your education.', ); }); + it('should direct to the schools and employers search tabs', () => { + cy.injectAxeThenAxeCheck(); + cy.contains('.comparison-tool-link', 'Schools and employers').click(); + cy.url().should('contain', '/schools-and-employers'); + cy.injectAxeThenAxeCheck(); + }); });