Repository: featureform/featureform (language: python)

Issue:
[Bug]: Search Results do not navigate to resource. 
### Expected Behavior

When the user uses search, the result list should navigate to a resource when clicked. 

### Actual Behavior

Clicking a search result does nothing. 

ex. 

https://user-images.githubusercontent.com/34227093/233747449-a7a6c6c3-cd46-427e-9817-d9890307ad59.mov



### Steps To Reproduce

1. Type "user" into the search box. 
2. Click on a result. 
3. Kaboom. Nothing.

### What mode are you running Featureform in?

Local

### What version of Python are you running?

3.7

### Featureform Python Package Version

0.7.2

### Featureform Helm Chart Version

_No response_

### Kubernetes Version

_No response_

### Relevant log output

_No response_


Discussion / hints:

Fix patch:
diff --git a/dashboard/package.json b/dashboard/package.json
index 0f91d34ece..eda0993d87 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -16,7 +16,7 @@
     "@reduxjs/toolkit": "^1.6.2",
     "@testing-library/jest-dom": "^4.2.4",
     "@testing-library/react": "^12.1.5",
-    "@testing-library/user-event": "^7.2.1",
+    "@testing-library/user-event": "^14.4.3",
     "chart.js": "^2.9.4",
     "chartjs-color": "^2.1.0",
     "chartjs-plugin-datasource-prometheus": "^1.0.11",
@@ -71,6 +71,7 @@
     ]
   },
   "devDependencies": {
+    "@testing-library/dom": "^9.2.0",
     "@testing-library/jest-dom": "^5.16.4",
     "@wojtekmaj/enzyme-adapter-react-17": "^0.6.3",
     "enzyme": "^3.11.0",
diff --git a/dashboard/src/components/breadcrumbs/BreadCrumbs.js b/dashboard/src/components/breadcrumbs/BreadCrumbs.js
index 9ebb0f01f0..1a7b3b419c 100644
--- a/dashboard/src/components/breadcrumbs/BreadCrumbs.js
+++ b/dashboard/src/components/breadcrumbs/BreadCrumbs.js
@@ -25,7 +25,8 @@ const useStyles = makeStyles((theme) => ({
 const BreadCrumbs = () => {
   const classes = useStyles();
   const { asPath } = useRouter();
-  const path = asPath.split("/");
+  const sansQuery = asPath.split('?').shift();
+  const path = sansQuery.split('/');
   while (path.length > 0 && path[0].length === 0) {
     path.shift();
   }
diff --git a/dashboard/src/components/entitypage/EntityPage.js b/dashboard/src/components/entitypage/EntityPage.js
index e23c755345..7f249f23db 100644
--- a/dashboard/src/components/entitypage/EntityPage.js
+++ b/dashboard/src/components/entitypage/EntityPage.js
@@ -26,6 +26,7 @@ function mapStateToProps(state) {
 
 const LoadingDots = () => {
   return (
+    <div data-testid='loadingDotsId'>
     <Container maxWidth="xl">
       <Paper elevation={3}>
         <Container maxWidth="sm">
@@ -33,6 +34,7 @@ const LoadingDots = () => {
         </Container>
       </Paper>
     </Container>
+    </div>
   );
 };
 
@@ -40,21 +42,25 @@ const checkIfEmpty = (object) => {
   return Object.keys(object).length === 0 && object.constructor === Object;
 };
 
+const fetchNotFound = (object) => {
+  return !object?.resources?.name && !object?.resources?.type
+}
+
 const EntityPage = ({ api, entityPage, activeVariants, type, entity, ...props }) => {
   let resourceType = Resource[Resource.pathToType[type]];
   const fetchEntity = props.fetch;
 
   useEffect(() => {
-    if (api && type && entity && resourceType) {
+    if (api && type && entity) {
       fetchEntity(api, type, entity);
     }
   }, [type, entity]);
 
   return (
     <div>
-      {entityPage.failed || !resourceType ? (
+      {entityPage.failed || (!entityPage.loading && fetchNotFound(entityPage)) ? (
         <NotFoundPage />
-      ) : checkIfEmpty(entityPage) || entityPage.loading ? (
+      ) : entityPage.loading || checkIfEmpty(entityPage) ? (
         <LoadingDots />
       ) : (
         <EntityPageView
diff --git a/dashboard/src/components/notfoundpage/NotFoundPage.js b/dashboard/src/components/notfoundpage/NotFoundPage.js
index c05eb6d2c9..90d2cebc0d 100644
--- a/dashboard/src/components/notfoundpage/NotFoundPage.js
+++ b/dashboard/src/components/notfoundpage/NotFoundPage.js
@@ -2,7 +2,7 @@ import React from "react";
 
 const NotFoundPage = () => {
   return (
-    <div>
+    <div data-testid='notFoundId'>
       <h1>404 Not Found :(</h1>
     </div>
   );
diff --git a/dashboard/src/components/resource-list/VariantSlice.js b/dashboard/src/components/resource-list/VariantSlice.js
index 45144afc5c..44db026bb6 100644
--- a/dashboard/src/components/resource-list/VariantSlice.js
+++ b/dashboard/src/components/resource-list/VariantSlice.js
@@ -17,11 +17,13 @@ const variantSlice = createSlice({
   reducers: {
     set: (state, action) => {
       const { type, name, variant } = action.payload;
-      var typePayload = type
-      if (Resource.typeToName[type] !== undefined) {
+      let typePayload = type
+      if (Resource.typeToName[type]) {
         typePayload = Resource.typeToName[type]
       }
-      state[typePayload][name] = variant;
+      if (state[typePayload]) {
+        state[typePayload][name] = variant;
+      }
     },
   },
 });
diff --git a/dashboard/src/components/search/SearchBar.js b/dashboard/src/components/search/SearchBar.js
index 065eae9ff5..d10af354da 100644
--- a/dashboard/src/components/search/SearchBar.js
+++ b/dashboard/src/components/search/SearchBar.js
@@ -2,11 +2,11 @@ import React from "react";
 import { createStyles, alpha, makeStyles } from "@material-ui/core/styles";
 import SearchIcon from "@material-ui/icons/Search";
 import Grid from "@material-ui/core/Grid";
-import Router from "next/router";
+import { useRouter } from "next/router";
 import Container from "@material-ui/core/Container";
 import InputBase from "@material-ui/core/InputBase";
 
-const ENTER_KEY_CODE = 13;
+const ENTER_KEY = 'Enter';
 
 const useStyles = makeStyles((theme) =>
   createStyles({
@@ -72,14 +72,15 @@ const useStyles = makeStyles((theme) =>
   })
 );
 
-const SearchBar = ({ input, setQuery, homePage }) => {
+const SearchBar = ({ homePage }) => {
   const classes = useStyles();
-
+  const router = useRouter();
   const [searchText, setSearchText] = React.useState("");
 
   function handleSearch(event) {
-    let uri = "/search?q=" + searchText;
-    Router.push(uri);
+    event.preventDefault();
+    let uri = "/search?q=" + searchText?.trim();
+    router.push(uri);
   }
 
   return (
@@ -92,19 +93,29 @@ const SearchBar = ({ input, setQuery, homePage }) => {
           <InputBase
             placeholder="Search..."
             onChange={(event) => {
-              setSearchText(event.target.value);
+              const rawText = event.target.value;
+              if (rawText === '') {
+                // user is deleting the text field. allow this and clear out state
+                setSearchText(rawText);
+                return;
+              }
+              const searchText = event.target.value ?? '';
+              if (searchText.trim()) {
+                setSearchText(searchText);
+              }
             }}
-            defaultValue={""}
-            onKeyDown={(event) =>
-              event.keyCode === ENTER_KEY_CODE && searchText.length > 0
-                ? handleSearch(event)
-                : ""
+            value={searchText}
+            onKeyDown={(event) => {
+              if (event.key === ENTER_KEY && searchText) {
+                handleSearch(event);
+              }
+            }
             }
             classes={{
               root: classes.inputRoot,
               input: homePage ? classes.inputInputHome : classes.inputTopBar,
             }}
-            inputProps={{ "aria-label": "search " }}
+            inputProps={{ "aria-label": "search", "data-testid": "searchInputId"}}
           />
         </Container>
       </Grid>
diff --git a/dashboard/src/components/searchresults/SearchResults.js b/dashboard/src/components/searchresults/SearchResults.js
index 0ce6ffdd68..4c711b272c 100644
--- a/dashboard/src/components/searchresults/SearchResults.js
+++ b/dashboard/src/components/searchresults/SearchResults.js
@@ -29,20 +29,11 @@ const SearchResults = ({ searchResults, api, setVariant, ...props }) => {
 
   return (
     <div>
-      {searchResults.resources ? (
-          <div>
-            {console.log("blah", searchResults.resources)}
         <SearchResultsView
-          results={searchResults.resources}
+          results={searchResults?.resources}
           search_query={search_query}
           setVariant={setVariant}
         />
-          </div>
-      ) : (
-        <div>
-          No Results
-        </div>
-      )}
     </div>
   );
 };
diff --git a/dashboard/src/components/searchresults/SearchResultsSlice.js b/dashboard/src/components/searchresults/SearchResultsSlice.js
index 1c774eabae..e371c7ff6a 100644
--- a/dashboard/src/components/searchresults/SearchResultsSlice.js
+++ b/dashboard/src/components/searchresults/SearchResultsSlice.js
@@ -4,8 +4,6 @@ export const fetchSearch = createAsyncThunk(
   "searchResults/fetchSearch",
   async ({ api, query }, { signal }) => {
     const response = await api.fetchSearch(query, signal);
-    console.log("got search")
-    console.log(response)
     return response;
   }
 );
diff --git a/dashboard/src/components/searchresults/SearchResultsView.js b/dashboard/src/components/searchresults/SearchResultsView.js
index bc4d961750..a9fc1e20af 100644
--- a/dashboard/src/components/searchresults/SearchResultsView.js
+++ b/dashboard/src/components/searchresults/SearchResultsView.js
@@ -7,9 +7,8 @@ import List from "@material-ui/core/List";
 import ListItem from "@material-ui/core/ListItem";
 import ListItemText from "@material-ui/core/ListItemText";
 import Container from "@material-ui/core/Container";
-import Icon from "@material-ui/core/Icon";
 import Resource from "../../api/resources/Resource.js";
-import { useRouter } from 'next/router'
+import { useRouter } from "next/router"
 
 function TabPanel(props) {
   const { children, value, index, ...other } = props;
@@ -70,7 +69,7 @@ const SearchResultsView = ({ results, search_query, setVariant }) => {
               variant="h4"
               style={{ display: "flex" }}
           >
-            {results.length > 0 ? (
+            {results?.length > 0 ? (
                 <div style={{ color: "gray" }}>Results for:&nbsp;</div>
             ) : (
                 <div style={{ color: "gray" }}>No results for:&nbsp;</div>
@@ -87,14 +86,16 @@ const SearchResultsView = ({ results, search_query, setVariant }) => {
 const SearchResultsList = ({ type, contents, setVariant }) => {
   const classes = useStyles();
   let filteredContentHits = {};
-  console.log(contents)
-  let moreFilteredContents = contents.filter((content) => {
-    if (content.Name + "." + content.Variant + "." + content.Type in filteredContentHits) {
-      return false;
-    }
-    filteredContentHits[content.Name + "." + content.Variant + "." + content.Type] = content.Variant;
-    return true;
-  });
+  let moreFilteredContents = [];
+  if (contents?.length) {
+    moreFilteredContents = contents.filter((content) => {
+      if (content.Name + "." + content.Variant + "." + content.Type in filteredContentHits) {
+        return false;
+      }
+      filteredContentHits[content.Name + "." + content.Variant + "." + content.Type] = content.Variant;
+      return true;
+    }); 
+  }
   return (
       <div>
         <List className={classes.root} component="nav">
@@ -112,42 +113,39 @@ const SearchResultsList = ({ type, contents, setVariant }) => {
 };
 
 var searchTypeMap = {
-  "FEATURE": "Feature", //feature variant
-  '\u0005': "Label",
-  '\u0006': "TrainingSet",
-  '\u0007': "Source",
-  '\u0008': "Provider",
-  '\u0009': "Entity",
-  '\u000a': "Model",
-  '\u000b': "User",
+  "FEATURE": "Feature",
+  "FEATURE_VARIANT": "Feature",
+  "LABEL": "Label",
+  "LABEL_VARIANT": "Label",
+  "TRAINING_SET": "TrainingSet",
+  "TRAINING_SET_VARIANT": "TrainingSet",
+  "SOURCE": "Source",
+  "SOURCE_VARIANT": "Source",
+  "PROVIDER": "Provider",
+  "ENTITY": "Entity",
+  "MODEL": "Model",
+  "USER": "User",
 }
 
 const SearchResultsItem = ({ type, content, setVariant }) => {
   const classes = useStyles();
   const router = useRouter()
-  console.log("content", content)
-  console.log("type", type)
-  // const resourceType = Resource[searchTypeMap[content.Type]];
-  console.log("found resource type")
-  // console.log(resourceType)
-  // const resourceIcon = resourceType.materialIcon;
+  const resourceType = Resource[searchTypeMap[content.Type]];
   function handleClick(content) {
-    if (resourceType.hasVariants) {
+    if (resourceType?.hasVariants) {
       setVariant(content.Type, content.Name, content.Variant);
     }
     router.push(resourceType.urlPathResource(content.Name));
   }
 
   return (
-      <div>
-        <ListItem button alignItems="flex-start">
+      <>
+        <ListItem button alignItems="flex-start" onClick={() => handleClick(content)} >
           <ListItemText
               primary={
                 <div>
                   <div>
                     <div className={classes.resultTitle}>
-                      {console.log("CLASS")}
-                      {/*<Icon>{resourceIcon}</Icon>*/}
                     </div>
                     <Typography className={classes.resultTitle} variant="h6">
                       {content.Name}
@@ -163,10 +161,9 @@ const SearchResultsItem = ({ type, content, setVariant }) => {
                   </Typography>
                 </div>
               }
-              onClick={() => handleClick(content)}
           />
         </ListItem>
-      </div>
+      </>
   );
 };
 
diff --git a/dashboard/src/components/topbar/TopBar.js b/dashboard/src/components/topbar/TopBar.js
index aaa26ff219..a792328a6c 100644
--- a/dashboard/src/components/topbar/TopBar.js
+++ b/dashboard/src/components/topbar/TopBar.js
@@ -80,6 +80,11 @@ export default function TopBar() {
     setAnchorEl(null);
   };
 
+  const goHome = (event) => {
+    event?.preventDefault();
+    router.push('/');
+  }
+
   return (
     <div className={classes.root}>
       <AppBar position="static" className={classes.appbar}>
@@ -89,8 +94,10 @@ export default function TopBar() {
               src="/static/FeatureForm_Logo_Full_Black.svg"
               height={30}
               alt="Featureform"
+              onClick={goHome}
               component="div"
               nowrap={"true"}
+              style={{cursor: 'pointer'}}
               sx={{ flexGrow: 1, display: { xs: "none", sm: "block" } }}
             />
           </div>
diff --git a/dashboard/yarn.lock b/dashboard/yarn.lock
index dee030356a..8734a63579 100644
--- a/dashboard/yarn.lock
+++ b/dashboard/yarn.lock
@@ -1035,6 +1035,20 @@
     lz-string "^1.4.4"
     pretty-format "^27.0.2"
 
+"@testing-library/dom@^9.2.0":
+  version "9.2.0"
+  resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.2.0.tgz#0e1f45e956f2a16f471559c06edd8827c4832f04"
+  integrity sha512-xTEnpUKiV/bMyEsE5bT4oYA0x0Z/colMtxzUY8bKyPXBNLn/e0V4ZjBZkEhms0xE4pv9QsPfSRu9AWS4y5wGvA==
+  dependencies:
+    "@babel/code-frame" "^7.10.4"
+    "@babel/runtime" "^7.12.5"
+    "@types/aria-query" "^5.0.1"
+    aria-query "^5.0.0"
+    chalk "^4.1.0"
+    dom-accessibility-api "^0.5.9"
+    lz-string "^1.5.0"
+    pretty-format "^27.0.2"
+
 "@testing-library/jest-dom@^4.2.4":
   version "4.2.4"
   resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-4.2.4.tgz#00dfa0cbdd837d9a3c2a7f3f0a248ea6e7b89742"
@@ -1059,10 +1073,10 @@
     "@testing-library/dom" "^8.0.0"
     "@types/react-dom" "<18.0.0"
 
-"@testing-library/user-event@^7.2.1":
-  version "7.2.1"
-  resolved "https://registry.npmjs.org/@testing-library/user-event/-/user-event-7.2.1.tgz"
-  integrity sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA==
+"@testing-library/user-event@^14.4.3":
+  version "14.4.3"
+  resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.3.tgz#af975e367743fa91989cd666666aec31a8f50591"
+  integrity sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==
 
 "@tootallnate/once@2":
   version "2.0.0"
@@ -3528,7 +3542,7 @@ lru-cache@^6.0.0:
   dependencies:
     yallist "^4.0.0"
 
-lz-string@^1.4.4:
+lz-string@^1.4.4, lz-string@^1.5.0:
   version "1.5.0"
   resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz"
   integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==


Test patch:
diff --git a/dashboard/__tests__/breadCrumbs.test.js b/dashboard/__tests__/breadCrumbs.test.js
new file mode 100644
index 0000000000..1bd7e5f483
--- /dev/null
+++ b/dashboard/__tests__/breadCrumbs.test.js
@@ -0,0 +1,90 @@
+import React from "react";
+import "jest-canvas-mock";
+import { cleanup, render } from "@testing-library/react";
+import BreadCrumbs from "../src/components/breadcrumbs/BreadCrumbs";
+
+const userRouterMock = {
+    asPath: "/"
+}
+
+jest.mock("next/router", () => ({
+    useRouter: () => userRouterMock
+}));
+
+describe("Bread Crumb Tests", () => {
+    beforeEach(() => {
+        jest.resetAllMocks();
+    })
+
+    afterEach(() => {
+        cleanup();
+    })
+
+    const getTestBody = (pathParam = "/") => {
+        userRouterMock.asPath = pathParam;
+        return <>
+            <BreadCrumbs />
+        </>
+    }
+
+    test("Issue-762: The breadcrumb component renders the ULR path correctly.", async () => {
+        //given: 
+        let urlPath = "/training-sets/fraud_training/last_model";
+        const helper = render(getTestBody(urlPath));
+
+        //when: we find the breadcrumb links
+        const anchorLinks = helper.container.querySelectorAll("a");
+        const finalBread = helper.container.querySelector("b");
+
+        //then: the bread crumbs are constructed correctly
+        expect(anchorLinks.length).toBe(3);
+        expect(anchorLinks[0].textContent).toBe("Home");
+        expect(anchorLinks[0].href).toBe("http://localhost/");
+
+        expect(anchorLinks[1].textContent).toBe("Training-sets");
+        expect(anchorLinks[1].href).toBe("http://localhost/training-sets");
+
+        expect(anchorLinks[2].textContent).toBe("Fraud_training");
+        expect(anchorLinks[2].href).toBe("http://localhost/training-sets/fraud_training");
+
+        expect(finalBread.textContent).toBe("Last_model");
+    });
+
+    test("Issue-762: Query params are ignored", async () => {
+        //given: a url path with a query param
+        let queryParam = "?q=shouldNotDisplayParam";
+        const helper = render(getTestBody(`/path1${queryParam}`));
+
+        //when: we find the breadcrumb links
+        const anchorLinks = helper.container.querySelectorAll("a");
+        const finalBread = helper.container.querySelector("b");
+        const foundQueryParam = helper.queryByText(queryParam);
+
+        //then: the query param is not present
+        expect(anchorLinks.length).toBe(1);
+        expect(anchorLinks[0].href).toBe("http://localhost/");
+        expect(anchorLinks[0].textContent).toBe("Home");
+        expect(anchorLinks[0].href).toBe("http://localhost/");
+        expect(finalBread.textContent).toBe("Path1");
+        expect(foundQueryParam).toBeNull();
+    });
+
+    test.each`
+    PathParam                       | LinkCountParam    | LastLinkParam
+    ${"/path"}                      | ${1}              | ${"http://localhost/"}
+    ${"/path1/path2/model1"}        | ${3}              | ${"http://localhost/path1/path2"}          
+    ${"/path1/path2/path3/model1"}  | ${4}              | ${"http://localhost/path1/path2/path3"}
+    `(`Issue-762: The route path "$PathParam" has $LinkCountParam anchor links`, async ({ PathParam, LinkCountParam, LastLinkParam }) => {
+        //given:
+        userRouterMock.asPath = PathParam;
+        const helper = render(getTestBody(PathParam));
+
+        //when: 
+        const anchorLinks = helper.container.querySelectorAll("a");
+        const lastAnchor = Array.from(anchorLinks).pop();
+
+        //then:
+        expect(anchorLinks.length).toBe(LinkCountParam);
+        expect(lastAnchor.href).toBe(LastLinkParam)
+    });
+});
diff --git a/dashboard/__tests__/entityPage.test.js b/dashboard/__tests__/entityPage.test.js
new file mode 100644
index 0000000000..771b92a875
--- /dev/null
+++ b/dashboard/__tests__/entityPage.test.js
@@ -0,0 +1,107 @@
+import React from "react";
+import "jest-canvas-mock";
+import { cleanup, render } from "@testing-library/react";
+import EntityPage from "../src/components/entitypage/EntityPage";
+import ReduxWrapper from "../src/components/redux/wrapper/ReduxWrapper";
+import { createSlice, configureStore } from "@reduxjs/toolkit";
+import { ThemeProvider } from "@material-ui/core/styles";
+import TEST_THEME from "../src/styles/theme";
+
+jest.mock("../src/components/entitypage/EntityPageView", () => () => {
+    return <div data-testid="entityPageViewId" />;
+});
+
+describe("Entity Page Tests", () => {
+    const LOADING_DOTS_ID = "loadingDotsId";
+    const NOT_FOUND = "notFoundId";
+    const apiMock = { fetchEntity: jest.fn() };
+
+    const defaultState = Object.freeze({
+        entityPage: { loading: false, failed: false },
+        selectedVariant: "",
+    })
+
+    const getTestBody = (initialState = {}) => {
+        const slice = createSlice({
+            name: "testSlice",
+            initialState: initialState
+        });
+        const store = configureStore({
+            reducer: slice.reducer,
+        });
+
+        return <>
+            <ReduxWrapper store={store}>
+                <ThemeProvider theme={TEST_THEME}>
+                    <EntityPage api={apiMock} type="sources" entity="myEntity" />
+                </ThemeProvider>
+            </ReduxWrapper>
+        </>;
+    }
+
+    beforeEach(() => {
+        jest.resetAllMocks();
+    })
+
+    afterEach(() => {
+        cleanup();
+    })
+
+    test("Issue-762: If the entity page fetch is loading, the loading component displays.", async () => {
+        //given:
+        const state = { ...defaultState, entityPage: { loading: true, failed: false } };
+        const helper = render(getTestBody(state));
+
+        //when: 
+        const loadingDotsDiv = await helper.findByTestId(LOADING_DOTS_ID);
+
+        //then: 
+        expect(loadingDotsDiv).toBeDefined();
+        expect(loadingDotsDiv.nodeName).toBe("DIV");
+        expect(apiMock.fetchEntity).toHaveBeenCalledTimes(1);
+    });
+
+    test('Issue-762: If the fetch state fails, load the "404 not found" component', async () => {
+        //given:
+        const state = { ...defaultState, entityPage: { failed: true, loading: false } };
+        const helper = render(getTestBody(state));
+
+        //when: 
+        const notFoundDiv = await helper.findByTestId(NOT_FOUND);
+        const foundFoundElem = helper.getByText("404", { exact: false })
+
+        //then: 
+        expect(notFoundDiv).toBeDefined();
+        expect(foundFoundElem.nodeName).toBe("H1");
+        expect(apiMock.fetchEntity).toHaveBeenCalledTimes(1);
+    });
+
+    test('Issue-762: The fetch completed, but the returned object is empty, load the "404 not found" component', async () => {
+        //given:
+        const state = { ...defaultState, entityPage: { failed: false, loading: false } };
+        const helper = render(getTestBody(state));
+
+        //when: 
+        const notFoundDiv = await helper.findByTestId(NOT_FOUND);
+        const foundFoundElem = helper.getByText("404", { exact: false })
+
+        //then: 
+        expect(notFoundDiv).toBeDefined();
+        expect(foundFoundElem.nodeName).toBe("H1");
+        expect(apiMock.fetchEntity).toHaveBeenCalledTimes(1);
+    });
+
+    test("Issue-762: The fetch completed, and the returned object is populated, display the entity view component", async () => {
+        //given:
+        const foundObj = { name: "a name", type: "a type" }
+        const state = { ...defaultState, entityPage: { failed: false, loading: false, resources: foundObj } };
+        const helper = render(getTestBody(state));
+
+        //when: 
+        const foundPageMock = await helper.findByTestId("entityPageViewId");
+
+        //then: 
+        expect(foundPageMock).toBeDefined();
+        expect(apiMock.fetchEntity).toHaveBeenCalledTimes(1);
+    });
+});
diff --git a/dashboard/__tests__/search.test.js b/dashboard/__tests__/search.test.js
new file mode 100644
index 0000000000..ca5a7cae92
--- /dev/null
+++ b/dashboard/__tests__/search.test.js
@@ -0,0 +1,94 @@
+import React from "react";
+import "jest-canvas-mock";
+import { cleanup, fireEvent, render } from "@testing-library/react";
+import SearchBar from "../src/components/search/SearchBar";
+import { ThemeProvider } from "@material-ui/core";
+import TEST_THEME from "../src/styles/theme";
+import userEvent from "@testing-library/user-event";
+
+const userRouterMock = {
+    push: jest.fn()
+}
+
+jest.mock("next/router", () => ({
+    useRouter: () => userRouterMock
+}));
+
+describe("Search Input Tests", () => {
+    const SEARCH_INPUT_ID = "searchInputId";
+    const SEARCH_URI = "/search?q=";
+    const USER_EVENT_ENTER = "{enter}";
+    const USER_EVENT_DELETE = "{backspace}";
+
+    const getTestBody = () => {
+        return <>
+            <ThemeProvider theme={TEST_THEME}>
+                <SearchBar homePage />
+            </ThemeProvider>
+        </>
+    }
+
+    beforeEach(() => {
+        jest.resetAllMocks();
+    })
+
+    afterEach(() => {
+        cleanup();
+    })
+
+    test("Issue-762: A basic search string invokes the router.", async () => {
+        //given:
+        const searchText = "Anthony";
+        const helper = render(getTestBody());
+
+        //when: the user types and hits enter
+        const searchField = helper.getByTestId(SEARCH_INPUT_ID);
+        await userEvent.type(searchField, `${searchText}${USER_EVENT_ENTER}`);
+
+        //then: a router call is invoked
+        expect(userRouterMock.push).toHaveBeenCalledTimes(1);
+        expect(userRouterMock.push).toHaveBeenCalledWith(`${SEARCH_URI}${searchText}`);
+    });
+
+    test("Issue-762: The user can incrementally delete the search input", async () => {
+        //given:
+        const searchText = "Anthony";
+        const helper = render(getTestBody());
+
+        //and: an initial search term is entered
+        const searchField = helper.getByTestId(SEARCH_INPUT_ID);
+        fireEvent.change(searchField, { target: { value: searchText } });
+        expect(searchField.value).toBe(searchText);
+
+        //when: the user deletes all the characters one by one, then presses enter
+        await userEvent.type(searchField, USER_EVENT_DELETE.repeat(searchText.length));
+        await userEvent.type(searchField, USER_EVENT_ENTER);
+
+        //then: the field is empty, and the enter invoked no router.push calls
+        expect(searchField.value).toBe("");
+        expect(userRouterMock.push).toHaveBeenCalledTimes(0);
+    });
+
+    test.each`
+    SearchInputParam        | InvokeParam
+    ${"noSpaceSearch"}      | ${1}
+    ${"spaces between"}     | ${1}
+    ${"spaces after   "}    | ${1}
+    ${"  spaces before"}    | ${1}
+    ${""}                   | ${0}
+    ${" "}                  | ${0}
+    `(`Issue-762: The search entry "$SearchInputParam" invokes the router: $InvokeParam times`, async ({ SearchInputParam, InvokeParam }) => {
+        //given:
+        const helper = render(getTestBody());
+
+        //when: the user initiates a search
+        const searchField = helper.getByTestId(SEARCH_INPUT_ID);
+        await userEvent.type(searchField, `${SearchInputParam}${USER_EVENT_ENTER}`);
+
+        //then:
+        expect(userRouterMock.push).toHaveBeenCalledTimes(InvokeParam);
+        if (InvokeParam) {
+            expect(userRouterMock.push).toHaveBeenCalledWith(`${SEARCH_URI}${SearchInputParam.trim()}`);
+        }
+    });
+});