diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 073df732e..b274a32cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,7 @@ repos: rev: v4.4.0 hooks: - id: trailing-whitespace + exclude: ^__snapshots__/ - id: end-of-file-fixer - id: check-yaml diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json index c0299d8ad..4e6fae071 100644 --- a/ui/.eslintrc.json +++ b/ui/.eslintrc.json @@ -1,15 +1,5 @@ { - "root": true, - "extends": ["next/core-web-vitals"], - "overrides": [ - { - "files": ["*.js"], - "parser": "espree", - "parserOptions": { - "ecmaVersion": 2020 - } - } - ], + "extends": ["next", "next/core-web-vitals", "prettier"], "rules": { "react/display-name": "off", "react-hooks/exhaustive-deps": "off" diff --git a/ui/README.md b/ui/README.md index ff41fa7be..475a08566 100644 --- a/ui/README.md +++ b/ui/README.md @@ -1,4 +1,10 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +# SCOAP# Repository + +www.repo.scoap3.org + +SCOAP3 is a one-of-its-kind partnership of over three-thousand libraries, key funding agencies and research centers in 43 countries and 3 intergovernmental organizations. Working with leading publishers, SCOAP3 has converted key journals in the field of high-energy physics to open access at no cost for authors. SCOAP3 centrally pays publishers for costs involved in providing their services, publishers, in turn, reduce subscription fees to all their customers, who can redirect these funds to contribute to SCOAP3. Each country contributes in a way commensurate to its scientific output in the field. In addition, existing open access journals are also centrally supported, removing any existing financial barrier for authors. + +SCOAP3 journals are open for any scientist to publish without any financial barriers. Copyright stays with authors, and a permissive CC-BY license allows text- and data-mining. SCOAP3 addresses open access mandates at no burden for authors. All articles appear in the SCOAP3 repository for further distribution, as well as being open access on publishers’ websites. ## Getting Started @@ -15,7 +21,3 @@ bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. diff --git a/ui/jest.config.mjs b/ui/jest.config.mjs new file mode 100644 index 000000000..0448fb0c4 --- /dev/null +++ b/ui/jest.config.mjs @@ -0,0 +1,13 @@ +import nextJest from 'next/jest.js' + +const createJestConfig = nextJest({ + dir: './', +}) + +/** @type {import('jest').Config} */ +const config = { + setupFilesAfterEnv: ['./jest.setup.ts'], + testEnvironment: 'jest-environment-jsdom', +} + +export default createJestConfig(config) diff --git a/ui/jest.setup.ts b/ui/jest.setup.ts new file mode 100644 index 000000000..09a5a99b2 --- /dev/null +++ b/ui/jest.setup.ts @@ -0,0 +1,26 @@ +import '@testing-library/jest-dom'; + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +jest.mock("next/navigation", () => { + return { + useRouter: jest.fn(() => ({ + push: jest.fn(), + })), + useSearchParams: jest.fn(() => ({ + get: jest.fn(), + })), + }; +}); diff --git a/ui/package.json b/ui/package.json index 8183b5d81..5c0dfef9b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -2,14 +2,17 @@ "name": "ui", "version": "0.1.0", "private": true, + "license": "GPL-2.0", "scripts": { "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint && pre-commit run --all-files", + "test": "yarn lint && jest && yarn build" }, "dependencies": { "@ant-design/cssinjs": "^1.17.2", + "@testing-library/user-event": "^14.5.1", "antd": "^5.10.1", "better-react-mathjax": "^2.0.3", "lodash.isequal": "^4.5.0", @@ -22,12 +25,18 @@ "react-vis": "^1.12.1" }, "devDependencies": { + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.1.2", + "@types/jest": "^29.5.10", "@types/lodash.isequal": "^4.5.8", "@types/node": "^20", "@types/react-html-parser": "^2.0.4", "autoprefixer": "^10", "eslint": "^8", "eslint-config-next": "13.5.5", + "eslint-config-prettier": "^9.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "postcss": "^8", "tailwindcss": "^3", "typescript": "^5" diff --git a/ui/src/__tests__/404.test.tsx b/ui/src/__tests__/404.test.tsx new file mode 100644 index 000000000..97e1491f4 --- /dev/null +++ b/ui/src/__tests__/404.test.tsx @@ -0,0 +1,12 @@ +import { render, screen } from "@testing-library/react"; + +import PageNotFound from "@/pages/404"; + +describe("PageNotFound", () => { + it("renders 404 page", () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + expect(screen.getByText("Page not found")).toBeInTheDocument(); + }); +}); diff --git a/ui/src/__tests__/500.test.tsx b/ui/src/__tests__/500.test.tsx new file mode 100644 index 000000000..5a1b1cc2e --- /dev/null +++ b/ui/src/__tests__/500.test.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { useRouter } from "next/navigation"; + +import ServerErrorPage from "@/pages/500"; + +jest.mock("next/navigation", () => ({ + useRouter: jest.fn(), +})); + +const mockPush = jest.fn(); +(useRouter as jest.Mock).mockImplementation(() => ({ + push: mockPush, +})); + +describe("ServerErrorPage", () => { + it("renders error page", () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + expect(screen.getByText("Something went wrong")).toBeInTheDocument(); + expect(screen.getByTestId("go-back")).toBeInTheDocument(); + }); + + it("triggers router.push on 'go to home page' button click", () => { + render(); + + fireEvent.click(screen.getByTestId("go-back")); + + expect(mockPush).toHaveBeenCalledWith("/"); + }); +}); diff --git a/ui/src/__tests__/__snapshots__/404.test.tsx.snap b/ui/src/__tests__/__snapshots__/404.test.tsx.snap new file mode 100644 index 000000000..afc2e95bd --- /dev/null +++ b/ui/src/__tests__/__snapshots__/404.test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PageNotFound renders 404 page 1`] = ` +
+
+ + + +

+ Page not found +

+

+ The page you are looking for could not be found. +

+
+
+`; diff --git a/ui/src/__tests__/__snapshots__/500.test.tsx.snap b/ui/src/__tests__/__snapshots__/500.test.tsx.snap new file mode 100644 index 000000000..e3439d3c1 --- /dev/null +++ b/ui/src/__tests__/__snapshots__/500.test.tsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ServerErrorPage renders error page 1`] = ` +
+
+ + + +

+ Something went wrong +

+

+ Please try again later or + + + go to home page + +

+
+
+`; diff --git a/ui/src/__tests__/__snapshots__/index.test.tsx.snap b/ui/src/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000..2990fe6d9 --- /dev/null +++ b/ui/src/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,206 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HomePage renders homepage with correct articles count 1`] = ` +
+ +
+
+

+ Search + + 4321 + Open Access + + articles: +

+ + + + + + + + +
+
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+`; diff --git a/ui/src/__tests__/__snapshots__/record.test.tsx.snap b/ui/src/__tests__/__snapshots__/record.test.tsx.snap new file mode 100644 index 000000000..3bdb07840 --- /dev/null +++ b/ui/src/__tests__/__snapshots__/record.test.tsx.snap @@ -0,0 +1,293 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RecordPage renders article details correctly 1`] = ` +
+
+
+
+
+

+ + Strings from 3D gravity: asymptotic dynamics of AdS 3 gravity with free boundary conditions + +

+

+ + + + Luis Apolo + + + + ( + + Department of Physics & The Oskar Klein Centre, Stockholm University, AlbaNova University Centre, SE-106 91, Stockholm, Sweden + + ) + + + ; + + + Bo Sundborg + + + + ( + + Department of Physics & The Oskar Klein Centre, Stockholm University, AlbaNova University Centre, SE-106 91, Stockholm, Sweden + + ) + +

+

+ + Pure three-dimensional gravity in anti-de Sitter space can be formulated as an SL(2 , R ) × SL(2 , R ) Chern-Simons theory, and the latter can be reduced to a WZW theory at the boundary. In this paper we show that AdS 3 gravity with free boundary conditions is described by a string at the boundary whose target spacetime is also AdS 3 . While boundary conditions in the standard construction of Coussaert, Henneaux, and van Driel are enforced through constraints on the WZW currents, we find that free boundary conditions are partially enforced through the string Virasoro constraints. + +

+
+
+
+
+
+ +
+
+
+
+
+
+
+ Published on: +
+
+ 25 June 2015 +
+
+ Created on: +
+
+ 30 April 2018 +
+
+ Publisher: +
+
+ Springer/SISSA +
+
+ Published in: +
+
+

+ + Journal of High Energy Physics + + + (2015) + + + Pages 171-189 + +

+
+
+ Article ID: + 123456 +
+
+
+ DOI + : +
+
+ + 10.1007/JHEP06(2015)171 + +
+
+
+
+ arXiv + : +
+
+ hep-th +
+
+ + 1504.07579 + +
+
+
+ Copyrights: +
+
+ The Author(s) +
+
+ Licence: +
+
+ + CC-BY-4.0 + +
+
+
+
+
+`; diff --git a/ui/src/__tests__/__snapshots__/search.test.tsx.snap b/ui/src/__tests__/__snapshots__/search.test.tsx.snap new file mode 100644 index 000000000..662aa8bb6 --- /dev/null +++ b/ui/src/__tests__/__snapshots__/search.test.tsx.snap @@ -0,0 +1,1988 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchPage renders search with correct articles count 1`] = ` +
+
+
+ +`; diff --git a/ui/src/__tests__/index.test.tsx b/ui/src/__tests__/index.test.tsx new file mode 100644 index 000000000..1d0b5b75d --- /dev/null +++ b/ui/src/__tests__/index.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from "@testing-library/react"; + +import HomePage from "@/pages/index"; +import { facets } from "@/mocks/index"; + +describe("HomePage", () => { + it("renders homepage with correct articles count", () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + expect(screen.getByText("4321 Open Access")).toBeInTheDocument(); + expect( + screen.getByText("Journal of High Energy Physics") + ).toBeInTheDocument(); + expect(screen.getByText("666")).toBeInTheDocument(); + }); + + it("renders correct information in Journals tab", () => { + render(); + + expect( + screen.getByText("Journal of High Energy Physics") + ).toBeInTheDocument(); + expect(screen.getByText("666")).toBeInTheDocument(); + }); + + it("renders correct information in Partners tab", () => { + render(); + + expect(screen.getByText("Poland")).toBeInTheDocument(); + expect(screen.getByText("2137")).toBeInTheDocument(); + }); +}); diff --git a/ui/src/__tests__/record.test.tsx b/ui/src/__tests__/record.test.tsx new file mode 100644 index 000000000..3be0a34e9 --- /dev/null +++ b/ui/src/__tests__/record.test.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { MathJaxContext } from "better-react-mathjax"; + +import RecordPage from "@/pages/records/[recordId]"; +import { record } from "@/mocks/index"; + +describe("RecordPage", () => { + it("renders article details correctly", () => { + const { container } = render( + + + + ); + + expect(container).toMatchSnapshot(); + expect( + screen.getByText( + "Strings from 3D gravity: asymptotic dynamics of AdS 3 gravity with free boundary conditions" + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "Pure three-dimensional gravity in anti-de Sitter space can be formulated as an SL(2 , R ) × SL(2 , R ) Chern-Simons theory, and the latter can be reduced to a WZW theory at the boundary. In this paper we show that AdS 3 gravity with free boundary conditions is described by a string at the boundary whose target spacetime is also AdS 3 . While boundary conditions in the standard construction of Coussaert, Henneaux, and van Driel are enforced through constraints on the WZW currents, we find that free boundary conditions are partially enforced through the string Virasoro constraints." + ) + ).toBeInTheDocument(); + }); +}); diff --git a/ui/src/__tests__/search.test.tsx b/ui/src/__tests__/search.test.tsx new file mode 100644 index 000000000..0be332ba2 --- /dev/null +++ b/ui/src/__tests__/search.test.tsx @@ -0,0 +1,58 @@ +import { render, screen } from "@testing-library/react"; +import { MathJaxContext } from "better-react-mathjax"; + +import SearchPage from "@/pages/search"; +import { facets, results, query } from "@/mocks/index"; + +describe("SearchPage", () => { + it("renders search with correct articles count", () => { + const { container } = render( + + + + ); + + expect(container).toMatchSnapshot(); + expect(screen.getByText("Found 10 results.")).toBeInTheDocument(); + }); + + it("renders facets", () => { + render( + + + + ); + + expect(screen.getByText("Year")).toBeInTheDocument(); + expect( + screen.getByText("Country / Region / Territory") + ).toBeInTheDocument(); + expect(screen.getByText("Journal")).toBeInTheDocument(); + }); + + it("renders pagination if >20 results", () => { + const { container } = render( + + + + ); + + const pagination = container.getElementsByClassName("ant-pagination"); + expect(pagination.length).toBe(2); + }); +}); diff --git a/ui/src/components/shared/JsonPreview.tsx b/ui/src/components/detail/JsonPreview.tsx similarity index 87% rename from ui/src/components/shared/JsonPreview.tsx rename to ui/src/components/detail/JsonPreview.tsx index 54cf39d11..77d44f85b 100644 --- a/ui/src/components/shared/JsonPreview.tsx +++ b/ui/src/components/detail/JsonPreview.tsx @@ -15,7 +15,7 @@ export const JsonPreview = ({ article }: JsonPreviewProps) => { key: "1", label: "Metadata preview. Preview of JSON metadata for this article.", children: ( -

+

                 {JSON.stringify(article, undefined, 2)}
               
diff --git a/ui/src/components/detail/__tests__/DetailPageInfo.test.tsx b/ui/src/components/detail/__tests__/DetailPageInfo.test.tsx new file mode 100644 index 000000000..2afc9c171 --- /dev/null +++ b/ui/src/components/detail/__tests__/DetailPageInfo.test.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import DetailPageInfo from "../DetailPageInfo"; +import { record } from '@/mocks/record'; + +describe("DetailPageInfo", () => { + it("renders detail page info correctly", () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + expect(screen.getByText("Published on:")).toBeInTheDocument(); + expect(screen.getByText("25 June 2015")).toBeInTheDocument(); + expect(screen.getByText("Created on:")).toBeInTheDocument(); + expect(screen.getByText("30 April 2018")).toBeInTheDocument(); + expect(screen.getByText("Springer/SISSA")).toBeInTheDocument(); + expect(screen.getByText("Published in:")).toBeInTheDocument(); + expect(screen.getByText("Journal of High Energy Physics")).toBeInTheDocument(); + expect(screen.getByText("(2015)")).toBeInTheDocument(); + expect(screen.getByText("Pages 171-189")).toBeInTheDocument(); + expect(screen.getByText("DOI:")).toBeInTheDocument(); + expect(screen.getByText("10.1007/JHEP06(2015)171")).toBeInTheDocument(); + expect(screen.getByText("arXiv:")).toBeInTheDocument(); + expect(screen.getByText("hep-th")).toBeInTheDocument(); + expect(screen.getByText("1504.07579")).toBeInTheDocument(); + expect(screen.getByText("Copyrights:")).toBeInTheDocument(); + expect(screen.getByText("The Author(s)")).toBeInTheDocument(); + expect(screen.getByText("Licence:")).toBeInTheDocument(); + expect(screen.getByText("CC-BY-4.0")).toBeInTheDocument(); + expect(screen.getByText("Fulltext files:")).toBeInTheDocument(); + expect(screen.getByText("XML")).toBeInTheDocument(); + expect(screen.getByText("PDF/A")).toBeInTheDocument(); + }); +}); diff --git a/ui/src/components/detail/__tests__/JsonPreview.test.tsx b/ui/src/components/detail/__tests__/JsonPreview.test.tsx new file mode 100644 index 000000000..52ebd2163 --- /dev/null +++ b/ui/src/components/detail/__tests__/JsonPreview.test.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { JsonPreview } from "../JsonPreview"; +import { record } from "@/mocks/index"; + +describe("JsonPreview", () => { + it("renders JSON preview correctly", async () => { + render(); + + await waitFor(() => + userEvent.click( + screen.getByText( + "Metadata preview. Preview of JSON metadata for this article." + ) + ) + ); + + expect( + screen.getByText( + "Metadata preview. Preview of JSON metadata for this article." + ) + ).toBeInTheDocument(); + expect(screen.getByTestId("json-preview-content")?.textContent).toContain( + "Strings from 3D gravity: asymptotic dynamics of AdS 3 gravity with free boundary conditions" + ); + expect(screen.getByTestId("json-preview-content")?.textContent).toContain( + "Sundborg" + ); + expect(screen.getByTestId("json-preview-content")?.textContent).toContain( + "2015-06-25" + ); + expect(screen.getByTestId("json-preview-content")?.textContent).toContain( + "Springer/SISSA" + ); + expect(screen.getByTestId("json-preview-content")?.textContent).toContain( + "Journal of High Energy Physics" + ); + expect(screen.getByTestId("json-preview-content")?.textContent).toContain( + "10.1007/JHEP06(2015)171" + ); + }); + + it("collapses and expands the content correctly", async () => { + render(); + + expect(screen.queryByTestId("json-preview-content")).toBeNull(); + + userEvent.click( + screen.getByText( + "Metadata preview. Preview of JSON metadata for this article." + ) + ); + + await waitFor(() => + expect(screen.getByTestId("json-preview-content")?.textContent).toContain( + "Strings from 3D gravity: asymptotic dynamics of AdS 3 gravity with free boundary conditions" + ) + ); + }); +}); diff --git a/ui/src/components/detail/__tests__/__snapshots__/DetailPageInfo.test.tsx.snap b/ui/src/components/detail/__tests__/__snapshots__/DetailPageInfo.test.tsx.snap new file mode 100644 index 000000000..ad5c1e39f --- /dev/null +++ b/ui/src/components/detail/__tests__/__snapshots__/DetailPageInfo.test.tsx.snap @@ -0,0 +1,166 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DetailPageInfo renders detail page info correctly 1`] = ` +
+
+
+ Published on: +
+
+ 25 June 2015 +
+
+ Created on: +
+
+ 30 April 2018 +
+
+ Publisher: +
+
+ Springer/SISSA +
+
+ Published in: +
+
+

+ + Journal of High Energy Physics + + + (2015) + + + Pages 171-189 + +

+
+
+ Article ID: + 123456 +
+
+
+ DOI + : +
+
+ + 10.1007/JHEP06(2015)171 + +
+
+
+
+ arXiv + : +
+
+ hep-th +
+
+ + 1504.07579 + +
+
+
+ Copyrights: +
+
+ The Author(s) +
+
+ Licence: +
+
+ + CC-BY-4.0 + +
+
+
+`; diff --git a/ui/src/components/home/__tests__/TabContent.test.tsx b/ui/src/components/home/__tests__/TabContent.test.tsx new file mode 100644 index 000000000..1a518b67c --- /dev/null +++ b/ui/src/components/home/__tests__/TabContent.test.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import TabContent from "../TabContent"; + +describe("TabContent", () => { + const mockData = [ + { key: "Item 1", doc_count: 5 }, + { key: "Item 2", doc_count: 10 }, + ]; + + it("renders tab content correctly", () => { + render(); + + expect(screen.getByText("Item 1")).toBeInTheDocument(); + expect(screen.getByText("Item 2")).toBeInTheDocument(); + expect(screen.getByText("5")).toBeInTheDocument(); + expect(screen.getByText("10")).toBeInTheDocument(); + }); + + it("triggers the correct journal link when clicking on an item", () => { + render(); + + userEvent.click(screen.getByText("Item 1")); + + expect(screen.getByText("Item 1")).toHaveAttribute("href", "/search?page=1&page_size=20&journal=Item 1"); + }); + + it("triggers the correct country link when clicking on an item", () => { + render(); + + userEvent.click(screen.getByText("Item 1")); + + expect(screen.getByText("Item 1")).toHaveAttribute("href", "/search?page=1&page_size=20&country=Item 1"); + }); +}); diff --git a/ui/src/components/search/CheckboxFacet.tsx b/ui/src/components/search/CheckboxFacet.tsx index d2b8cc82f..6582011b5 100644 --- a/ui/src/components/search/CheckboxFacet.tsx +++ b/ui/src/components/search/CheckboxFacet.tsx @@ -59,7 +59,7 @@ const CheckboxFacet: React.FC = ({
{displayedData?.map((item) => (
- + + {item?.doc_count}
))} diff --git a/ui/src/components/search/__tests__/Checkboxfacet.test.tsx b/ui/src/components/search/__tests__/Checkboxfacet.test.tsx new file mode 100644 index 000000000..8ad09070b --- /dev/null +++ b/ui/src/components/search/__tests__/Checkboxfacet.test.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; + +import CheckboxFacet from "../CheckboxFacet"; + +describe("CheckboxFacet", () => { + const mockData = [ + { key: "Item 1", doc_count: 5 }, + { key: "Item 2", doc_count: 10 }, + { key: "Item 3", doc_count: 15 }, + { key: "Item 4", doc_count: 20 }, + { key: "Item 5", doc_count: 25 }, + { key: "Item 6", doc_count: 30 }, + { key: "Item 7", doc_count: 35 }, + { key: "Item 8", doc_count: 40 }, + { key: "Item 9", doc_count: 45 }, + { key: "Item 10", doc_count: 50 }, + { key: "Item 11", doc_count: 55 }, + { key: "Item 12", doc_count: 60 }, + { key: "Item 13", doc_count: 65 }, + { key: "Item 14", doc_count: 70 }, + ]; + + const mockParams = { + country: "SelectedCountry", + }; + + it("renders checkbox facet correctly", () => { + render( + + ); + + expect(screen.getByText("Country")).toBeInTheDocument(); + expect(screen.getByText("Item 1")).toBeInTheDocument(); + expect(screen.getByText("Item 2")).toBeInTheDocument(); + expect(screen.getByText("5")).toBeInTheDocument(); + expect(screen.getByText("10")).toBeInTheDocument(); + }); + + it("triggers the correct state change when clicking on a checkbox ", () => { + render( + + ); + + const checkbox = screen.getByRole("checkbox", { name: "Item 1" }); + + fireEvent.click(checkbox); + + expect(checkbox).toBeChecked(); + + fireEvent.click(checkbox); + + expect(checkbox).not.toBeChecked(); + }); + + + it("displays additional items when clicking 'Show More' ", () => { + render( + + ); + + expect(screen.getByText("Item 1")).toBeInTheDocument(); + expect(screen.queryByText("Item 14")).not.toBeInTheDocument(); + + fireEvent.click(screen.getByText("Show More")); + + expect(screen.getByText("Item 1")).toBeInTheDocument(); + expect(screen.getByText("Item 14")).toBeInTheDocument(); + }); + + it("hides additional items when clicking 'Show Less'", () => { + render( + + ); + + fireEvent.click(screen.getByText("Show More")); + + expect(screen.getByText("Item 1")).toBeInTheDocument(); + expect(screen.getByText("Item 14")).toBeInTheDocument(); + + fireEvent.click(screen.getByText("Show Less")); + + expect(screen.getByText("Item 1")).toBeInTheDocument(); + expect(screen.queryByText("Item 14")).not.toBeInTheDocument(); + }); +}); diff --git a/ui/src/components/search/__tests__/ResultItem.test.tsx b/ui/src/components/search/__tests__/ResultItem.test.tsx new file mode 100644 index 000000000..7b24459e2 --- /dev/null +++ b/ui/src/components/search/__tests__/ResultItem.test.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { MathJaxContext } from "better-react-mathjax"; + +import ResultItem from "../ResultItem"; +import { record } from "@/mocks/index"; + +describe("ResultItem", () => { + it("renders result item correctly", () => { + render( + + + + ); + + expect( + screen.getByText( + "Strings from 3D gravity: asymptotic dynamics of AdS 3 gravity with free boundary conditions" + ) + ).toBeInTheDocument(); + expect(screen.getByText("Apolo, Luis")).toBeInTheDocument(); + expect( + screen.getByText("Journal of High Energy Physics") + ).toBeInTheDocument(); + expect(screen.getByText("10.1007/JHEP06(2015)171")).toBeInTheDocument(); + expect(screen.getByText("XML")).toBeInTheDocument(); + expect(screen.getByText("PDF/A")).toBeInTheDocument(); + }); + + it("renders link to the record page correctly", () => { + render( + + + + ); + + const link = screen.getByRole("link", { + name: /Strings from 3D gravity: asymptotic dynamics of AdS 3 gravity with free boundary conditions/i, + }); + expect(link).toHaveAttribute("href", "/records/10913"); + }); +}); diff --git a/ui/src/components/search/__tests__/SearchPagination.test.tsx b/ui/src/components/search/__tests__/SearchPagination.test.tsx new file mode 100644 index 000000000..60864115e --- /dev/null +++ b/ui/src/components/search/__tests__/SearchPagination.test.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { useRouter } from "next/navigation"; + +import SearchPagination from "../SearchPagination"; + +const mockParams = { + page: 2, + page_size: 10, +}; + +const mockCount = 100; + +jest.mock("next/navigation", () => ({ + useRouter: jest.fn(), +})); + +const mockPush = jest.fn(); +(useRouter as jest.Mock).mockImplementation(() => ({ + push: mockPush, +})); + +describe("SearchPagination", () => { + it("renders pagination correctly", () => { + const { container } = render( + + ); + + expect(container.querySelector(".ant-pagination")).toBeInTheDocument(); + expect( + screen + .getAllByRole("listitem") + .find((listitem) => listitem.textContent === "2") + ).toHaveClass("ant-pagination-item-active"); + }); + + it("triggers the correct navigation when clicking on a page", () => { + render(); + + fireEvent.click( + screen + .getAllByRole("listitem") + .find((listitem) => listitem.textContent === "3")! + ); + + expect(mockPush).toHaveBeenCalledWith("?page=3&page_size=10"); + }); +}); diff --git a/ui/src/components/search/__tests__/SearchResults.test.tsx b/ui/src/components/search/__tests__/SearchResults.test.tsx new file mode 100644 index 000000000..c20060afc --- /dev/null +++ b/ui/src/components/search/__tests__/SearchResults.test.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import SearchResults from "../SearchResults"; +import { Params, Result } from "@/types"; + +const mockResults = [ + { id: "1", title: "Result 1" }, + { id: "2", title: "Result 2" }, +] as never as Result[]; + +const mockCount = 20; +const mockParams = { + page: 1, + page_size: 10, +}; + +jest.mock("../ResultItem", () => ({ article }: { article: Result} ) => ( +
{article.title}
+)); + +jest.mock("../SearchPagination", () => ({ + count, + params, +}: { + count: number + params: Params +}) =>
{`${count} results, page ${params.page}`}
); + +describe("SearchResults", () => { + it("renders search results correctly", () => { + const { container } = render(); + + expect(screen.getByText("Found 20 results.")).toBeInTheDocument(); + expect(screen.getByTestId("result-item-1")).toBeInTheDocument(); + expect(screen.getByTestId("result-item-2")).toBeInTheDocument(); + expect(container.querySelector("[data-testid='search-pagination']")).toBeInTheDocument(); + }); +}); diff --git a/ui/src/components/search/__tests__/YearFacet.test.tsx b/ui/src/components/search/__tests__/YearFacet.test.tsx new file mode 100644 index 000000000..3bacf35f9 --- /dev/null +++ b/ui/src/components/search/__tests__/YearFacet.test.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; + +import YearFacet from "../YearFacet"; +import { PublicationYear } from "@/types"; + +describe("YearFacet", () => { + const mockData = [ + { key: "2021", doc_count: 5 }, + { key: "2022", doc_count: 10 }, + { key: "2023", doc_count: 15 }, + ] as never as PublicationYear[]; + + const mockParams = { + publication_year__range: "2021__2023", + }; + + it("renders year facet correctly", () => { + render(); + + expect(screen.getByText("Year")).toBeInTheDocument(); + expect(screen.getByText("2021")).toBeInTheDocument(); + expect(screen.getByText("2023")).toBeInTheDocument(); + }); + + it("displays the correct hint when hovering on a bar", () => { + const { container } = render( + + ); + + fireEvent.mouseOver(container.querySelector("rect")!); + + expect(screen.getByText("5")).toBeInTheDocument(); + + fireEvent.mouseOut(container.querySelector("rect")!); + + expect(screen.queryByText("5")).not.toBeInTheDocument(); + }); + + it("shows reset button after clicking on a bar", () => { + const { container } = render( + + ); + + fireEvent.click(container.querySelector("rect")!); + + expect(screen.getByText("Reset")).toBeInTheDocument(); + }); +}); diff --git a/ui/src/components/shared/PublicationInfo.tsx b/ui/src/components/shared/PublicationInfo.tsx index bae14189e..79437e87c 100644 --- a/ui/src/components/shared/PublicationInfo.tsx +++ b/ui/src/components/shared/PublicationInfo.tsx @@ -26,7 +26,7 @@ const PublicationInfo: React.FC = ({ data, page }) => { } if (journal_volume) { - publicationText += `, Volume ${journal_volume}, Volume ${journal_volume}`; } if (volume_year) { diff --git a/ui/src/components/shared/__tests__/Authors.test.tsx b/ui/src/components/shared/__tests__/Authors.test.tsx new file mode 100644 index 000000000..40aeaf742 --- /dev/null +++ b/ui/src/components/shared/__tests__/Authors.test.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; + +import Authors from "../Authors"; +import { authors } from "@/mocks/index"; + +describe("Authors", () => { + it("renders authors correctly in search page", () => { + render(); + + expect(screen.getByText("Doe, John")).toBeInTheDocument(); + expect(screen.getByText("Smith, Jane")).toBeInTheDocument(); + expect(screen.getByTitle("1234-5678-9101-1126")).toBeInTheDocument(); + expect(screen.getByTitle("9876-5432-1098-7658")).toBeInTheDocument(); + }); + + it("renders authors correctly in detail page", () => { + render(); + + expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getByText("Jane Smith")).toBeInTheDocument(); + expect( + screen.getByText("Affiliation 1, Affiliation 2") + ).toBeInTheDocument(); + expect(screen.getByText("Affiliation 3")).toBeInTheDocument(); + expect(screen.getByTitle("1234-5678-9101-1126")).toBeInTheDocument(); + expect(screen.getByTitle("9876-5432-1098-7658")).toBeInTheDocument(); + }); + + it("renders affiliations link correctly", () => { + render(); + + expect( + screen.getByRole("link", { name: /Affiliation 1, Affiliation 2/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: /Affiliation 3/i }) + ).toBeInTheDocument(); + }); + + it("renders Orcid links correctly", () => { + render(); + + expect(screen.getByTitle("1234-5678-9101-1126")).toHaveAttribute( + "href", + "https://orcid.org/1234-5678-9101-1126" + ); + expect(screen.getByTitle("9876-5432-1098-7658")).toHaveAttribute( + "href", + "https://orcid.org/9876-5432-1098-7658" + ); + }); + + it("renders modal button correctly", () => { + render(); + + expect(screen.getByText(/Show all 7 authors/i)).toBeInTheDocument(); + }); + + it("opens and closes modal correctly", async () => { + render(); + + expect(screen.getByText("et al")).toBeInTheDocument(); + expect(screen.queryByText("Michael Smith")).toBeNull(); + expect(screen.queryByText("Tom Jones")).toBeNull(); + + fireEvent.click(screen.getByText(/Show all 7 authors/i)); + await waitFor(() => + expect(screen.getByText("Michael Smith")).toBeInTheDocument() + ); + await waitFor(() => + expect(screen.getByText("Tom Jones")).toBeInTheDocument() + ); + + fireEvent.click(screen.getByRole("button", { name: /Close/i })); + await waitFor(() => + expect(screen.queryByRole("button", { name: /Close/i })).toBeNull() + ); + }); +}); diff --git a/ui/src/components/shared/__tests__/Footer.test.tsx b/ui/src/components/shared/__tests__/Footer.test.tsx new file mode 100644 index 000000000..ba0bfa319 --- /dev/null +++ b/ui/src/components/shared/__tests__/Footer.test.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { render } from "@testing-library/react"; + +import Footer from "../Footer"; + +describe("Footer", () => { + it("renders footer correctly", () => { + const { container } = render(