diff --git a/package-lock.json b/package-lock.json index 165f2bc..eeb57ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,18 +8,22 @@ "name": "autocomplete-component", "version": "0.1.0", "dependencies": { - "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", - "@types/jest": "^27.5.2", - "@types/node": "^16.18.70", - "@types/react": "^18.2.47", - "@types/react-dom": "^18.2.18", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@testing-library/jest-dom": "5.17.0", + "@testing-library/react": "13.4.0", + "@testing-library/user-event": "13.5.0", + "@types/jest": "27.5.2", + "@types/node": "16.18.70", + "@types/react": "18.2.47", + "@types/react-dom": "18.2.18", + "react": "18.2.0", + "react-dom": "18.2.0", "react-scripts": "5.0.1", - "typescript": "^4.9.5", - "web-vitals": "^2.1.4" + "typescript": "4.9.5", + "web-vitals": "2.1.4" + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -648,9 +652,17 @@ } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, "engines": { "node": ">=6.9.0" }, @@ -1893,6 +1905,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", diff --git a/package.json b/package.json index a9c1d17..512b735 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@babel/plugin-transform-private-property-in-object": "^7.23.4", "@testing-library/jest-dom": "5.17.0", "@testing-library/react": "13.4.0", "@testing-library/user-event": "13.5.0", @@ -39,5 +40,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11" } } diff --git a/src/App.test.tsx b/src/App.test.tsx deleted file mode 100644 index 2a68616..0000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/components/autoComplete.test.tsx b/src/components/autoComplete.test.tsx new file mode 100644 index 0000000..cf2cb5b --- /dev/null +++ b/src/components/autoComplete.test.tsx @@ -0,0 +1,180 @@ +import { + render, + screen, + fireEvent, + waitFor, + act, + within, + cleanup, +} from "@testing-library/react"; +import AutoComplete from "./autoComplete"; + +afterEach(() => { + cleanup(); +}); + +describe("AutoComplete Component", () => { + it("renders the input box", () => { + render(); + const searchInput = screen.getByLabelText("search-input"); + expect(searchInput).toBeInTheDocument(); + }); + + it("renders the suggestion list on input focus", async () => { + render(); + + const suggestionList = screen.queryByTestId("suggestion-list"); + expect(suggestionList).not.toBeInTheDocument(); + + const searchInput = screen.getByLabelText("search-input"); + + act(() => { + searchInput.focus(); + }); + + await waitFor( + () => { + const showSuggestionList = screen.queryByTestId("suggestion-list"); + expect(showSuggestionList).toBeInTheDocument(); + }, + { timeout: 5000 } + ); + }); + + it("hides the suggestion list on click outside the text box", async () => { + render(); + + const suggestionList = screen.queryByTestId("suggestion-list"); + expect(suggestionList).not.toBeInTheDocument(); + + const searchInput = screen.getByLabelText("search-input"); + + // show the suggestion list + act(() => { + searchInput.focus(); + }); + + // confirm its showing + await waitFor( + () => { + const showSuggestionList = screen.getByTestId("suggestion-list"); + expect(showSuggestionList).toBeInTheDocument(); + }, + { timeout: 5000 } + ); + + // click out of the input box + fireEvent.click(document.body); + + // check that the suggestion list is nolonger showiung + await waitFor( + () => { + const showSuggestionList = screen.queryByTestId("suggestion-list"); + expect(showSuggestionList).not.toBeInTheDocument(); + }, + { timeout: 5000 } + ); + }); + + it("updates the suggestion list based on user input", async () => { + render(); + + const searchInput = screen.getByLabelText("search-input"); + const inputValue = "cle"; + + fireEvent.focus(searchInput); + + fireEvent.change(searchInput, { target: { value: inputValue } }); + + await waitFor( + () => { + const suggestions = screen.getAllByTestId("suggestion-list"); + const suggestionContent = suggestions.map((suggestion) => + suggestion.textContent?.toLocaleLowerCase().includes(inputValue) + ); + expect(suggestionContent).toBeTruthy(); + }, + { timeout: 5000 } + ); + }); + + it("populates the input field when a suggestion item is clicked", async () => { + render(); + const inputValue = "cle"; + const searchInput = screen.getByLabelText( + "search-input" + ) as HTMLInputElement; + let suggestionItems: HTMLElement[] = []; + + fireEvent.change(searchInput, { target: { value: inputValue } }); + + await waitFor( + () => { + const suggestions = screen.queryByTestId( + "suggestion-list" + ) as HTMLElement; + expect(suggestions).toBeInTheDocument(); + + suggestionItems = within(suggestions) + .getAllByRole("listitem") + .filter((value) => + value.textContent?.toLocaleLowerCase().includes(inputValue) + ); + }, + { timeout: 5000 } + ); + + fireEvent.click(suggestionItems[0]); + + await waitFor( + () => { + const searchInput = screen.getByLabelText( + "search-input" + ) as HTMLInputElement; + + expect(searchInput.value).toBe("Clementine Bauch"); + }, + { timeout: 5000 } + ); + }); + + it("should return a div with no-options if no suggestion list is empty", async () => { + render(); + const inputValue = "les"; + const searchInput = screen.getByLabelText( + "search-input" + ) as HTMLInputElement; + + fireEvent.change(searchInput, { target: { value: inputValue } }); + + await waitFor( + () => { + const noOption = screen.getByText("No options"); + expect(noOption).toBeInTheDocument(); + }, + { timeout: 5000 } + ); + }); + + it("moves selected suggestion move and down when using the keyboard arrow keys", async () => { + render(); + const inputValue = "c"; + const searchInput = screen.getByLabelText( + "search-input" + ) as HTMLInputElement; + let suggestions: HTMLElement | null = null; + + fireEvent.change(searchInput, { target: { value: inputValue } }); + + await waitFor( + () => { + const suggestions = screen.queryByTestId( + "suggestion-list" + ) as HTMLElement; + expect(suggestions).toBeInTheDocument(); + }, + { timeout: 5000 } + ); + if (suggestions) fireEvent.keyDown(suggestions, { key: "ArrowDown" }); + }); +}); diff --git a/src/components/autoComplete.tsx b/src/components/autoComplete.tsx index a2b7b5f..a0597a1 100644 --- a/src/components/autoComplete.tsx +++ b/src/components/autoComplete.tsx @@ -22,7 +22,7 @@ function AutoComplete() { const handleOnClick = (event: React.MouseEvent) => { setShowSuggestions(false); - setSearchText(event.currentTarget.innerText); + if(event.currentTarget.textContent) setSearchText(event.currentTarget.textContent); }; const handleKeyDown = (event: React.KeyboardEvent) => { diff --git a/src/components/autoCompleteState.tsx b/src/components/autoCompleteState.tsx index b81cecd..53fa38c 100644 --- a/src/components/autoCompleteState.tsx +++ b/src/components/autoCompleteState.tsx @@ -14,9 +14,9 @@ const AutoCompleteState = () => { useEffect(() => { (async () => { const filteredData = await FileteredData(debouncedInputValue); - if (filteredData) { - setSuggestions(filteredData); - } + if (!filteredData) return + + setSuggestions(filteredData); return filteredData; })(); diff --git a/src/components/inputField.tsx b/src/components/inputField.tsx index f7f8777..c0926bd 100644 --- a/src/components/inputField.tsx +++ b/src/components/inputField.tsx @@ -19,6 +19,7 @@ const InputField: FC = ({ = ({ showSuggestions, handleOnClick, selectedSuggestion, - searchText + searchText, }) => { if (showSuggestions) { return suggestions.length ? ( -
    +
      {suggestions.map((suggestion, index) => (
    • = ({ ))}
    ) : ( -
    No options
    +
    No options
    ); } else return null; };