diff --git a/__mocks__/combobox.js b/__mocks__/combobox.js new file mode 100644 index 0000000..31101bc --- /dev/null +++ b/__mocks__/combobox.js @@ -0,0 +1,14 @@ +export const ComboboxMockData = [ + { + id: 1, + name: "React", + }, + { + id: 2, + name: "Go", + }, + { + id: 3, + name: "Vanilla JS", + }, +]; diff --git a/__tests__/Unit/Componets/Combobox/Combobox.test.tsx b/__tests__/Unit/Componets/Combobox/Combobox.test.tsx new file mode 100644 index 0000000..f7031f6 --- /dev/null +++ b/__tests__/Unit/Componets/Combobox/Combobox.test.tsx @@ -0,0 +1,114 @@ +import React from "react"; +import { render, fireEvent, screen, waitFor } from "@testing-library/react"; +import { ComboboxMockData } from "../../../../__mocks__/combobox"; +import ComboboxDropdown from "@/components/Combobox"; + +describe("Combobox Component", () => { + it("should render with placeholder and dropdown closed", () => { + render(); + + const input = screen.getByRole("combobox"); + expect(input).toHaveAttribute("placeholder", "placeholder"); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is not visible + }); + + it("should open dropdown on button click and display the options", async () => { + render(); + + const button = screen.getByRole("button"); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is initially closed + + fireEvent.click(button); + + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for dropdown animation + + await screen.findByRole("listbox"); + + expect(screen.getByText("React")).toBeInTheDocument(); + expect(screen.getByText("Go")).toBeInTheDocument(); + expect(screen.getByText("Vanilla JS")).toBeInTheDocument(); + }); + + it("Select a value from the combobox list", async () => { + const onChangeMock = jest.fn(); + render(); + const button = screen.getByRole("button"); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is initially closed + + fireEvent.click(button); + + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for dropdown animation + + await screen.findByRole("listbox"); + + expect(screen.getByText("React")).toBeInTheDocument(); + expect(screen.getByText("Go")).toBeInTheDocument(); + expect(screen.getByText("Vanilla JS")).toBeInTheDocument(); + + fireEvent.click(screen.getByText("React")); + expect(onChangeMock).toHaveBeenCalledWith({ id: 1, name: "React" }); + }); + + it("filters the options based on input value", async () => { + render( {}} placeholder="placeholder" options={ComboboxMockData} />); + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + const button = screen.getByRole("button"); + fireEvent.click(button); + await screen.findByRole("listbox"); + + const input = screen.getByPlaceholderText("placeholder"); + fireEvent.change(input, { target: { value: "React", id: 1 } }); + + await waitFor(() => { + expect(screen.getByText("React")).toBeInTheDocument(); + }); + + screen.debug(); + }); + + it("Display No Results option when no options are present", async () => { + render(); + const input = screen.getByRole("combobox"); + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is initially closed + + fireEvent.change(input, { target: { value: "NonexistentSkill" } }); + + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for dropdown animation + + await screen.findByRole("listbox"); + + await screen.findByText("No Results"); + + expect(screen.getByText("No Results")).toBeInTheDocument(); + }); + + it("Close the combo box when clicked outside", async () => { + render( +
+ {}} placeholder="placeholder" options={ComboboxMockData} /> +
Outside Element
+
+ ); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + const button = screen.getByRole("button"); + fireEvent.click(button); + + // Ensure the dropdown is open + await screen.findByRole("listbox"); + expect(screen.getByRole("listbox")).toBeInTheDocument(); + + const outsideElement = screen.getByTestId("outside-element"); + fireEvent.mouseDown(outsideElement); + fireEvent.mouseUp(outsideElement); + fireEvent.click(outsideElement); + + // Wait for the dropdown to close + await waitFor(() => { + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + }); + }); +}); diff --git a/__tests__/Unit/Componets/SkillComboBox/SkillComboBox.test.tsx b/__tests__/Unit/Componets/SkillComboBox/SkillComboBox.test.tsx new file mode 100644 index 0000000..e7b06e6 --- /dev/null +++ b/__tests__/Unit/Componets/SkillComboBox/SkillComboBox.test.tsx @@ -0,0 +1,123 @@ +import React from "react"; +import { render, fireEvent, screen, waitFor } from "@testing-library/react"; +import SkillCombobox from "@/components/SkillComboBox"; +import { skillMockData } from "../../../../__mocks__/endorsements"; + +describe("SkillCombobox Component", () => { + it("should render with placeholder and dropdown closed", () => { + render(); + + const input = screen.getByRole("combobox"); + expect(input).toHaveAttribute("placeholder", "select skill"); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is not visible + }); + + it("should open dropdown on button click and display the options", async () => { + render(); + + const button = screen.getByRole("button"); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is initially closed + + fireEvent.click(button); + + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for dropdown animation + + await screen.findByRole("listbox"); + + expect(screen.getByText("React")).toBeInTheDocument(); + expect(screen.getByText("Go")).toBeInTheDocument(); + expect(screen.getByText("Vanilla JS")).toBeInTheDocument(); + }); + + it("Select a value from the combobox list", async () => { + const onChangeMock = jest.fn(); + render(); + const button = screen.getByRole("button"); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is initially closed + + fireEvent.click(button); + + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for dropdown animation + + await screen.findByRole("listbox"); + + expect(screen.getByText("React")).toBeInTheDocument(); + expect(screen.getByText("Go")).toBeInTheDocument(); + expect(screen.getByText("Vanilla JS")).toBeInTheDocument(); + + fireEvent.click(screen.getByText("React")); + expect(onChangeMock).toHaveBeenCalledWith({ id: 1, skill: "React" }); + }); + + it("filters the options based on input value", async () => { + render( {}} placeholder="select skill" options={skillMockData} />); + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + const button = screen.getByRole("button"); + fireEvent.click(button); + await screen.findByRole("listbox"); + + const input = screen.getByPlaceholderText("select skill"); + fireEvent.change(input, { target: { value: "React", id: 1 } }); + + await waitFor(() => { + expect(screen.getByText("React")).toBeInTheDocument(); + }); + + screen.debug(); + }); + + it("Display Add new Skill option when no options are present and add new skill click perform action", async () => { + const handleAddSkill = jest.fn(); + + render(); + const input = screen.getByRole("combobox"); + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is initially closed + + fireEvent.change(input, { target: { value: "NonexistentSkill" } }); + + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for dropdown animation + + await screen.findByRole("listbox"); + + await screen.findByText("Add New Skill"); + + await fireEvent.click(screen.getByText("Add New Skill")); + + expect(handleAddSkill).toHaveBeenCalled(); + }); + + it("Close the combo box when clicked outside", async () => { + render( +
+ {}} + placeholder="select skill" + options={skillMockData} + handleAddSkill={() => {}} + /> +
Outside Element
+
+ ); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + const button = screen.getByRole("button"); + fireEvent.click(button); + + // Ensure the dropdown is open + await screen.findByRole("listbox"); + expect(screen.getByRole("listbox")).toBeInTheDocument(); + + const outsideElement = screen.getByTestId("outside-element"); + fireEvent.mouseDown(outsideElement); + fireEvent.mouseUp(outsideElement); + fireEvent.click(outsideElement); + + // Wait for the dropdown to close + await waitFor(() => { + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + }); + }); +}); diff --git a/__tests__/components/combobox/Combobox.test.tsx b/__tests__/components/combobox/Combobox.test.tsx new file mode 100644 index 0000000..a551c6a --- /dev/null +++ b/__tests__/components/combobox/Combobox.test.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import { render, fireEvent, screen, waitFor } from "@testing-library/react"; +import { ComboboxMockData } from "../../../__mocks__/combobox"; +import ComboboxDropdown from "@/components/Combobox"; + +global.ResizeObserver = class { + observe() {} + unobserve() {} + disconnect() {} +}; + +describe("Combobox Component", () => { + it("Should display combobox with placeholder and Down arrow icon", async () => { + render(); + + expect(screen.getByRole("combobox")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("placeholder")).toBeInTheDocument(); + expect(screen.getByAltText("expand")).toBeInTheDocument(); + }); + + it("should open dropdown on button click and display the options", async () => { + render(); + + const button = screen.getByRole("button"); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is initially closed + + fireEvent.click(button); + + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for dropdown animation + + await screen.findByRole("listbox"); + + expect(screen.getByText("React")).toBeInTheDocument(); + expect(screen.getByText("Go")).toBeInTheDocument(); + expect(screen.getByText("Vanilla JS")).toBeInTheDocument(); + }); + + it("Select a value from the combobox list", async () => { + const onChangeMock = jest.fn(); + render(); + const button = screen.getByRole("button"); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is initially closed + + fireEvent.click(button); + + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for dropdown animation + + await screen.findByRole("listbox"); + + expect(screen.getByText("React")).toBeInTheDocument(); + expect(screen.getByText("Go")).toBeInTheDocument(); + expect(screen.getByText("Vanilla JS")).toBeInTheDocument(); + + fireEvent.click(screen.getByText("React")); + expect(onChangeMock).toHaveBeenCalledWith({ id: 1, name: "React" }); + }); + + it("filters the options based on input value", async () => { + render( {}} placeholder="placeholder" options={ComboboxMockData} />); + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + const button = screen.getByRole("button"); + fireEvent.click(button); + await screen.findByRole("listbox"); + + const input = screen.getByPlaceholderText("placeholder"); + fireEvent.change(input, { target: { value: "React", id: 1 } }); + + await waitFor(() => { + expect(screen.getByText("React")).toBeInTheDocument(); + }); + + screen.debug(); + }); + + it("Display No Results option when no options are present", async () => { + render(); + const input = screen.getByRole("combobox"); + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is initially closed + + fireEvent.change(input, { target: { value: "NonexistentSkill" } }); + + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for dropdown animation + + await screen.findByRole("listbox"); + + await screen.findByText("No Results"); + }); +}); diff --git a/__tests__/components/skillcombobox/SkillCombobox.test.tsx b/__tests__/components/skillcombobox/SkillCombobox.test.tsx new file mode 100644 index 0000000..6b6a14c --- /dev/null +++ b/__tests__/components/skillcombobox/SkillCombobox.test.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import { render, fireEvent, screen, waitFor } from "@testing-library/react"; +import SkillCombobox from "@/components/SkillComboBox"; +import Endorsements from "@/pages/endorsements"; +import { skillMockData } from "../../../__mocks__/endorsements"; + +global.ResizeObserver = class { + observe() {} + unobserve() {} + disconnect() {} +}; + +describe("SkillCombobox Component", () => { + it("skillbox compnent in the endorsement page", () => { + render(); + + const skillCombobox = screen.getByRole("combobox"); + + expect(skillCombobox).toBeInTheDocument(); + }); + + it("should open dropdown on button click and display the options", async () => { + render(); + + const button = screen.getByRole("button"); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is initially closed + + fireEvent.click(button); + + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for dropdown animation + + await screen.findByRole("listbox"); + + expect(screen.getByText("React")).toBeInTheDocument(); + expect(screen.getByText("Go")).toBeInTheDocument(); + expect(screen.getByText("Vanilla JS")).toBeInTheDocument(); + }); + + it("Select a value from the combobox list", async () => { + const onChangeMock = jest.fn(); + render(); + const button = screen.getByRole("button"); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is initially closed + + fireEvent.click(button); + + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for dropdown animation + + await screen.findByRole("listbox"); + + expect(screen.getByText("React")).toBeInTheDocument(); + expect(screen.getByText("Go")).toBeInTheDocument(); + expect(screen.getByText("Vanilla JS")).toBeInTheDocument(); + + fireEvent.click(screen.getByText("React")); + expect(onChangeMock).toHaveBeenCalledWith({ id: 1, skill: "React" }); + }); + + it("filters the options based on input value", async () => { + render( {}} placeholder="select skill" options={skillMockData} />); + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + const button = screen.getByRole("button"); + fireEvent.click(button); + await screen.findByRole("listbox"); + + const input = screen.getByPlaceholderText("select skill"); + fireEvent.change(input, { target: { value: "React", id: 1 } }); + + await waitFor(() => { + expect(screen.getByText("React")).toBeInTheDocument(); + }); + + screen.debug(); + }); + + it("Display Add new Skill option when no options are present", async () => { + render(); + const input = screen.getByRole("combobox"); + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); // Check listbox is initially closed + + fireEvent.change(input, { target: { value: "NonexistentSkill" } }); + + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for dropdown animation + + await screen.findByRole("listbox"); + + await screen.findByText("Add New Skill"); + }); +}); diff --git a/__tests__/pages/endorsements.tests.tsx b/__tests__/pages/endorsements.tests.tsx index 69224fd..3706a8d 100644 --- a/__tests__/pages/endorsements.tests.tsx +++ b/__tests__/pages/endorsements.tests.tsx @@ -4,6 +4,7 @@ import { render, screen } from "@testing-library/react"; describe("Endorsements", () => { test("renders Endorsements ui", () => { render(); + const skillCombobox = screen.getByRole("combobox"); const upvoteButton = screen.getByText("Upvote"); const downvoteButton = screen.getByText("Downvote"); const CompleteEndorsementButton = screen.getByText("Complete Endorsement"); @@ -11,6 +12,7 @@ describe("Endorsements", () => { expect(screen.getByText("Endorsements")).toBeInTheDocument(); expect(screen.getByText("search")).toBeInTheDocument(); expect(screen.getByTestId("input")).toBeInTheDocument(); + expect(skillCombobox).toBeInTheDocument(); expect(upvoteButton).toBeInTheDocument(); expect(downvoteButton).toBeInTheDocument(); expect(screen.getByPlaceholderText("placeholder text here")).toBeInTheDocument(); diff --git a/package.json b/package.json index 10ae3f4..cbf3228 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,15 @@ "test-watch": "jest --watch " }, "dependencies": { + "@headlessui/react": "^1.7.16", + "@heroicons/react": "^2.1.5", "@tanstack/react-query": "^4.33.0", "@tanstack/react-query-devtools": "^4.33.0", "@testing-library/react-hooks": "^8.0.1", "autoprefixer": "10.4.14", "axios": "^1.6.7", "classnames": "^2.5.1", + "clsx": "^2.1.1", "next": "13.4.4", "postcss": "8.4.23", "react": "18.2.0", diff --git a/public/addicon.svg b/public/addicon.svg new file mode 100644 index 0000000..9e54e97 --- /dev/null +++ b/public/addicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/downarrow.svg b/public/downarrow.svg new file mode 100644 index 0000000..6bca942 --- /dev/null +++ b/public/downarrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Combobox/index.tsx b/src/components/Combobox/index.tsx new file mode 100644 index 0000000..4c80319 --- /dev/null +++ b/src/components/Combobox/index.tsx @@ -0,0 +1,70 @@ +import Image from "next/image"; +import { Combobox, Transition } from "@headlessui/react"; +import React, { useState, Fragment } from "react"; + +type OptionTypes = { + id: number; + name: string; +}; + +type ComboboxProps = { + options: OptionTypes[]; + value?: OptionTypes; + onChange?: (value: OptionTypes) => void; + placeholder?: string; +}; + +const ComboboxDropdown = ({ options, value, onChange, placeholder }: ComboboxProps) => { + const [query, setQuery] = useState(""); + + const filteredOptions = !query + ? options + : options.filter((option) => option?.name?.toLowerCase().includes(query.toLowerCase())); + + return ( + +
+ option?.name} + onChange={(event) => setQuery(event.target.value)} + placeholder={placeholder} + /> + + expand + + setQuery("")} + > + + {filteredOptions?.length === 0 && query !== "" ? ( +
+ + No Results + +
+ ) : ( + filteredOptions?.map((option) => ( + + + {option?.name} + + + )) + )} +
+
+
+
+ ); +}; + +export default ComboboxDropdown; diff --git a/src/components/SkillComboBox/index.tsx b/src/components/SkillComboBox/index.tsx new file mode 100644 index 0000000..32ce618 --- /dev/null +++ b/src/components/SkillComboBox/index.tsx @@ -0,0 +1,75 @@ +import Image from "next/image"; +import { Combobox, Transition } from "@headlessui/react"; +import React, { useState, Fragment } from "react"; + +type OptionTypes = { + id: number; + skill: string; +}; + +type ComboboxProps = { + placeholder: string; + options: OptionTypes[]; + onChange?: (value: OptionTypes) => void; + value?: OptionTypes; + handleAddSkill?: () => void; +}; + +const SkillCombobox = ({ placeholder, options, onChange, value, handleAddSkill }: ComboboxProps) => { + const [query, setQuery] = useState(""); + + const filteredSkills = !query + ? options + : options.filter((option) => option?.skill?.toLowerCase().includes(query.toLowerCase())); + + return ( + +
+ option?.skill} + onChange={(event) => setQuery(event.target.value)} + placeholder={placeholder} + /> + + expand + + setQuery("")} + > + + {filteredSkills?.length === 0 && query !== "" ? ( +
+ + add skill icon + Add New Skill + +
+ ) : ( + filteredSkills?.map((option) => ( + + + {option?.skill} + + + )) + )} +
+
+
+
+ ); +}; + +export default SkillCombobox; diff --git a/src/pages/endorsements/index.tsx b/src/pages/endorsements/index.tsx index f477975..39baa4a 100644 --- a/src/pages/endorsements/index.tsx +++ b/src/pages/endorsements/index.tsx @@ -1,12 +1,27 @@ -import React, { FC } from "react"; -import { DropDown } from "@/components/DropDown"; +import React, { FC, useState } from "react"; import Layout from "@/components/Layout"; import SearchBox from "@/components/SearchBox"; import { BsHandThumbsUp, BsHandThumbsDown } from "react-icons/bs"; import SkillLabel from "@/components/SkillLabel"; -import { endorsementsListsMock } from "../../../__mocks__/endorsements"; +import { endorsementsListsMock, skillMockData } from "../../../__mocks__/endorsements"; +import SkillCombobox from "@/components/SkillComboBox"; + +type OptionTypes = { + id: number; + skill: string; +}; const Endorsements: FC = () => { + const [selected, setSelected] = useState(undefined); + + const handleSkillSelect = (value: OptionTypes) => { + setSelected(value); + }; + + const handleAddSkill = () => { + alert("Add skill clicked"); + }; + return (
@@ -29,10 +44,15 @@ const Endorsements: FC = () => {

Prakash

-

skill:

- +

Skill :

+
-

vote: