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 */}
-
-
- Use my location
-
-
- {distanceDropdownOptions.map(option => (
-
- {option.label}
-
- ))}
-
-
-
-
- );
-};
-
-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 ? (
+
+
+ Finding your location...
+
+ ) : (
+ <>
+ {/* eslint-disable-next-line @department-of-veterans-affairs/prefer-button-component */}
+
+
+ Use my location
+
+ >
+ )}
+ >
+ );
+};
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 => (
+
+ {option.label}
+
+ ))}
+
+
+
+
+ );
+};
+
+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();
+ });
});