diff --git a/_tests_/components/search/result.test.tsx b/_tests_/components/search/result.test.tsx new file mode 100644 index 0000000..c795025 --- /dev/null +++ b/_tests_/components/search/result.test.tsx @@ -0,0 +1,29 @@ +import { render, screen } from "@testing-library/react"; +import Result from "../../../src/components/search/result"; + +describe("Result Component", () => { + const mockProps = { + transactionId: "123", + definition: "Test definition", + category: "Test category", + term: "Test term" + }; + + it("renders the result component with all props", () => { + render(); + + const container = screen.getByTitle("search-result-content"); + expect(container).toBeInTheDocument(); + expect(screen.getByText("Test term")).toBeInTheDocument(); + expect(screen.getByText("Test definition")).toBeInTheDocument(); + expect(screen.getByText("Test category")).toBeInTheDocument(); + }); + + it("renders with missing props", () => { + render(); + + const container = screen.getByTitle("search-result-content"); + expect(container).toBeInTheDocument(); + expect(screen.getByText("Test term")).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/_tests_/components/search/unavailable-result.test.tsx b/_tests_/components/search/unavailable-result.test.tsx new file mode 100644 index 0000000..5913eca --- /dev/null +++ b/_tests_/components/search/unavailable-result.test.tsx @@ -0,0 +1,38 @@ +import { render, screen } from "@testing-library/react"; +import UnavailableResult from "../../../src/components/search/unavailable-result"; +import { useTranslation } from "next-i18next"; + +// Mock next-i18next +jest.mock("next-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe("UnavailableResult Component", () => { + it("renders the unavailable result component", () => { + const term = "nonexistent-term"; + render(); + + const container = screen.getByTitle("unavailable-search-result"); + expect(container).toBeInTheDocument(); + expect(screen.getByText(term)).toBeInTheDocument(); + expect(screen.getByText("twitter")).toBeInTheDocument(); + expect(screen.getByText("gitHubIssueText")).toBeInTheDocument(); + }); + + it("generates correct twitter link", () => { + const term = "test-term"; + render(); + + const twitterLink = screen.getByText("twitter").closest("a"); + expect(twitterLink).toHaveAttribute( + "href", + expect.stringContaining("Glossetadotcom") + ); + expect(twitterLink).toHaveAttribute( + "href", + expect.stringContaining(term) + ); + }); +}); \ No newline at end of file diff --git a/_tests_/pages/search/index.test.tsx b/_tests_/pages/search/index.test.tsx new file mode 100644 index 0000000..4b652fd --- /dev/null +++ b/_tests_/pages/search/index.test.tsx @@ -0,0 +1,50 @@ +import { render, screen } from "@testing-library/react"; +import SearchPage from "../../../src/pages/search"; + +// Mock matchMedia +beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); +}); + +// Mock next/router +jest.mock('next/router', () => ({ + useRouter() { + return { + route: '/', + pathname: '', + query: '', + asPath: '', + push: jest.fn(), + }; + }, +})); + +describe("Search Page", () => { + it("renders the search page", () => { + render(); + + // Test for main components that should be present + expect(screen.getByRole("main")).toBeInTheDocument(); + expect(screen.getByTitle("glosseta-search-fallback-page")).toBeInTheDocument(); + }); + + it("displays loading skeleton", () => { + render(); + + // Test for skeleton loading elements using class name + const skeletons = document.getElementsByClassName("chakra-skeleton"); + expect(skeletons.length).toBeGreaterThan(0); + }); +}); \ No newline at end of file diff --git a/_tests_/pages/search/term/[id].test.jsx b/_tests_/pages/search/term/[id].test.jsx index c2eb16d..db9a18c 100644 --- a/_tests_/pages/search/term/[id].test.jsx +++ b/_tests_/pages/search/term/[id].test.jsx @@ -3,7 +3,15 @@ import { render, screen } from "@testing-library/react"; import SearchResults from "../../../../src/pages/search/term/[id]"; jest.mock("react-i18next", () => ({ - useTranslation: () => ({ t: (key) => key }), + useTranslation: () => ({ + t: (key) => ({ + definition: 'definition', + unavailableSearchResultDescription: 'unavailableSearchResultDescription', + apiFetchErrorText: 'apiFetchErrorText', + requestThisTerm: 'requestThisTerm', + // Add other keys as needed + }[key] || key), + }), })); jest.spyOn(require("next/router"), "useRouter").mockImplementation(() => ({ @@ -30,18 +38,24 @@ describe("Search Results - Success", () => { /> ); - const searchBarInput = screen.getByTitle("search-bar-input"); - const resultContainer = screen.getByTitle("search-result-content"); - const resultUnavailableContainer = screen.queryByTitle( - "unavailable-search-result" + // Check for the term heading + const termHeading = screen.getByRole('heading', { name: term.toUpperCase() }); + expect(termHeading).toBeInTheDocument(); + + // Check for definition section + const definitionHeading = screen.getByRole('heading', { name: 'definition' }); + expect(definitionHeading).toBeInTheDocument(); + + // Use getAllByText and check the specific one we want + const definitionTexts = screen.getAllByText(definition); + const definitionContent = definitionTexts.find( + element => element.tagName.toLowerCase() === 'p' ); - const apiErrorContainer = screen.queryByTitle("api-error-result"); + expect(definitionContent).toBeInTheDocument(); - expect(searchBarInput).toBeInTheDocument(); - expect(resultUnavailableContainer).not.toBeInTheDocument(); - expect(apiErrorContainer).not.toBeInTheDocument(); - - expect(resultContainer).toBeInTheDocument(); + // Check that error and unavailable content is not present + expect(screen.queryByText('unavailableSearchResultDescription')).not.toBeInTheDocument(); + expect(screen.queryByText('apiFetchErrorText')).not.toBeInTheDocument(); }); }); @@ -65,21 +79,15 @@ describe("Search Results - Not found", () => { /> ); - const searchBarInput = screen.getByTitle("search-bar-input"); - const resultContainer = screen.queryByTitle("search-result-content"); - const resultUnavailableContainer = screen.getByTitle( - "unavailable-search-result" - ); - const apiErrorContainer = screen.queryByTitle("api-error-result"); - - expect(searchBarInput).toBeInTheDocument(); - expect(resultContainer).not.toBeInTheDocument(); - expect(apiErrorContainer).not.toBeInTheDocument(); + // Check for unavailable content + const termHeading = screen.getByRole('heading', { name: term.toUpperCase() }); + expect(termHeading).toBeInTheDocument(); + + expect(screen.getByText('unavailableSearchResultDescription')).toBeInTheDocument(); + expect(screen.getByText('requestThisTerm')).toBeInTheDocument(); - expect(resultUnavailableContainer).toBeInTheDocument(); - expect(resultUnavailableContainer).toHaveTextContent( - "unavailableSearchResultDescription" - ); + // Check that success content is not present + expect(screen.queryByRole('heading', { name: 'definition' })).not.toBeInTheDocument(); }); }); @@ -103,18 +111,11 @@ describe("Search Results - Error", () => { /> ); - const searchBarInput = screen.getByTitle("search-bar-input"); - const resultContainer = screen.queryByTitle("search-result-content"); - const resultUnavailableContainer = screen.queryByTitle( - "unavailable-search-result" - ); - const apiErrorContainer = screen.getByTitle("api-error-result"); - - expect(searchBarInput).toBeInTheDocument(); - expect(resultContainer).not.toBeInTheDocument(); - expect(resultUnavailableContainer).not.toBeInTheDocument(); + // Check for error content + expect(screen.getByText('apiFetchErrorText')).toBeInTheDocument(); - expect(apiErrorContainer).toBeInTheDocument(); - expect(apiErrorContainer).toHaveTextContent("apiFetchErrorText"); + // Check that success and unavailable content is not present + expect(screen.queryByRole('heading', { name: term.toUpperCase() })).not.toBeInTheDocument(); + expect(screen.queryByText('unavailableSearchResultDescription')).not.toBeInTheDocument(); }); }); diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 5514be9..a554756 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -14,12 +14,21 @@ "searchIconAriaLabel" : "Magnifying glass image", "searchInputAriaLabel" : "Search web3 terms here", "searchTermNotFoundInFilter" : "Term not found", + "copy" : "Copy", + "share" : "Share", + "moreLearningMaterials" : "More Learning Materials", + "definition" : "Definition", + "comingSoon" : "Coming soon! We're working on bringing you additional learning resources and related terms to enhance your understanding.", + "unavailableSearchResultDescription" : "We couldn't find this term in our glossary yet. Would you like to request it be added?", + "back" : "Back", + "searchForAnotherTerm" : "Search for Another Term", + "requestThisTerm" : "Request This Term", "searchResultContentSourceHeading" : "Content Source", "searchResultContentSourceDescription" : "The definition you see above is stored on the Arweave network which is a protocol for storing data permanently in a decentralized manner among network users who have storage to spare. As a result, this definition will live forever on the Arweave network.", "searchResultContentSourceTransactionLinkText" : "Click here to view the Arweave transaction for this definition", - "glossaryPageHeading" : "Glossary", + "glossaryPageDescription" : "Explore the comprehensive glossary of web3 terms, including definitions, explanations, and related concepts. Whether you're a beginner or an expert, our glossary provides a detailed overview of the terminology used in blockchain, NFTs, and cryptocurrency.", "somethingMissingHeading" : "Something missing?", "twitterTermRequestDescription" : "If there's a specific term you were looking for but didn't see above, please let us know and we'll get it added. Give us a shout on ", "glossaryTermFetchErrorHeading": "Error", diff --git a/src/components/glossary/scroll-to-top-button.tsx b/src/components/glossary/scroll-to-top-button.tsx index 65d0b4a..a073c07 100644 --- a/src/components/glossary/scroll-to-top-button.tsx +++ b/src/components/glossary/scroll-to-top-button.tsx @@ -26,16 +26,25 @@ export const ScrollToTopButton = ({ title="glossary-scroll-to-top-button" onClick={scrollToTheTop} position="fixed" - bottom="20px" - right="30px" + bottom="6" + right="6" zIndex="99" - backgroundColor="#A3A3A3" - fontSize="18px" - textColor="black" - cursor="pointer" - _hover={{ backgroundColor: "#BABABA" }} + colorScheme="blackAlpha" + size="lg" + rounded="full" + shadow="lg" + aria-label={t("scrollToTheTopButton")} + _hover={{ + transform: "translateY(-2px)", + shadow: "xl" + }} + _active={{ + transform: "translateY(0)", + shadow: "md" + }} + transition="all 0.2s" > - + {t("scrollToTheTopButton")} diff --git a/src/components/input/search-bar.tsx b/src/components/input/search-bar.tsx index ba55954..aaf8ad0 100644 --- a/src/components/input/search-bar.tsx +++ b/src/components/input/search-bar.tsx @@ -16,16 +16,16 @@ import Trie from "../../filter/trie"; import AutocompleteFilter from "../../filter/autocomplete"; const SearchBar = ({ - baseWidth, - smWidth, - mdWidth, - lgWidth, + baseWidth = "280px", + smWidth = "320px", + mdWidth = "400px", + lgWidth = "480px", filterItems, }: { - baseWidth: string; - smWidth: string; - mdWidth: string; - lgWidth: string; + baseWidth?: string; + smWidth?: string; + mdWidth?: string; + lgWidth?: string; filterItems: any[]; }): JSX.Element => { const [searchTerm, setSearchTerm] = useState(""); @@ -131,7 +131,7 @@ const SearchBar = ({ { event.currentTarget.scrollIntoView(false); }} @@ -173,54 +184,63 @@ const SearchBar = ({ {filteredSuggestions.length > 0 && - filteredSuggestions.map((suggestion, index) => { - return ( - <> - { - setSearchTerm(event.currentTarget.innerText); - setFilteredSuggestions([]); - setShowSuggestions(false); - setActiveSuggestion(0); - location.assign( - `/search/term/${event.currentTarget.innerText - .trim() - .toLowerCase()}` - ); - }} - background={ - index === activeSuggestion ? "darkgray" : "white" - } - padding={1} - > - - {suggestion} - - - ); - })} + filteredSuggestions.map((suggestion, index) => ( + { + setSearchTerm(event.currentTarget.innerText); + setFilteredSuggestions([]); + setShowSuggestions(false); + setActiveSuggestion(0); + location.assign( + `/search/term/${event.currentTarget.innerText + .trim() + .toLowerCase()}` + ); + }} + background={ + index === activeSuggestion ? "blue.50" : "white" + } + color={index === activeSuggestion ? "blue.700" : "gray.700"} + padding={3} + rounded="md" + cursor="pointer" + _hover={{ + background: "blue.50", + color: "blue.700" + }} + transition="all 0.2s" + > + + {suggestion} + + ))} {filteredSuggestions.length === 0 && ( <> - - + + {t("searchTermNotFoundInFilter")} diff --git a/src/components/layout/page/index.tsx b/src/components/layout/page/index.tsx index c2c11fe..beac365 100644 --- a/src/components/layout/page/index.tsx +++ b/src/components/layout/page/index.tsx @@ -8,13 +8,13 @@ const PageLayout = ({ children }: { children?: object }): JSX.Element => { <>