From 379572497976db92fdc274135b999d244a0db616 Mon Sep 17 00:00:00 2001 From: aldbr Date: Sun, 15 Oct 2023 11:42:41 +0200 Subject: [PATCH 1/2] feat: add dark mode --- src/app/dashboard/jobmonitor/page.tsx | 9 +- src/app/dashboard/page.tsx | 8 +- src/app/globals.css | 23 --- src/app/layout.tsx | 6 +- src/components/applications/JobMonitor.tsx | 32 ++++ src/components/applications/UserDashboard.tsx | 32 ++++ src/components/layout/Dashboard.tsx | 120 ++++++++------- src/components/layout/Showcase.tsx | 140 +++++++++--------- src/components/ui/DashboardButton.tsx | 10 +- src/components/{data => ui}/JobDataGrid.tsx | 0 src/components/ui/LoginButton.tsx | 1 - src/components/ui/ThemeToggleButton.tsx | 22 +++ src/contexts/ThemeProvider.tsx | 32 ++++ src/hooks/jobs.tsx | 7 +- src/hooks/theme.tsx | 29 ++++ test/DashboardAppBar.test.tsx | 65 -------- test/ThemeToggleButton.test.tsx | 56 +++++++ 17 files changed, 364 insertions(+), 228 deletions(-) create mode 100644 src/components/applications/JobMonitor.tsx create mode 100644 src/components/applications/UserDashboard.tsx rename src/components/{data => ui}/JobDataGrid.tsx (100%) create mode 100644 src/components/ui/ThemeToggleButton.tsx create mode 100644 src/contexts/ThemeProvider.tsx create mode 100644 src/hooks/theme.tsx delete mode 100644 test/DashboardAppBar.test.tsx create mode 100644 test/ThemeToggleButton.test.tsx diff --git a/src/app/dashboard/jobmonitor/page.tsx b/src/app/dashboard/jobmonitor/page.tsx index 6e35f4b..54fc0ed 100644 --- a/src/app/dashboard/jobmonitor/page.tsx +++ b/src/app/dashboard/jobmonitor/page.tsx @@ -1,10 +1,5 @@ -import { JobDataGrid } from "@/components/data/JobDataGrid"; +import JobMonitor from "@/components/applications/JobMonitor"; export default async function Page() { - return ( -
-

Job Monitor

- -
- ); + return ; } diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index f171f6c..6d5d744 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,7 +1,5 @@ +import UserDashboard from "@/components/applications/UserDashboard"; + export default function Page() { - return ( -
- Hello User -
- ); + return ; } diff --git a/src/app/globals.css b/src/app/globals.css index 05c1ecd..e69de29 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,23 +0,0 @@ -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 255, 255, 255; - --background-end-rgb: 255, 255, 255; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e4b1ce0..657ccd2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,6 @@ -import "./globals.css"; import { Inter } from "next/font/google"; import { OIDCProvider } from "@/components/auth/OIDCUtils"; +import { ThemeProvider } from "@/contexts/ThemeProvider"; const inter = Inter({ subsets: ["latin"] }); @@ -21,7 +21,9 @@ export default function RootLayout({ return ( - {children} + + {children} + ); diff --git a/src/components/applications/JobMonitor.tsx b/src/components/applications/JobMonitor.tsx new file mode 100644 index 0000000..615511d --- /dev/null +++ b/src/components/applications/JobMonitor.tsx @@ -0,0 +1,32 @@ +"use client"; +import * as React from "react"; +import CssBaseline from "@mui/material/CssBaseline"; +import { Box } from "@mui/material"; +import { useMUITheme } from "@/hooks/theme"; +import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles"; +import { JobDataGrid } from "../ui/JobDataGrid"; + +/** + * Build the Job Monitor application + * @returns Job Monitor content + */ +export default function JobMonitor() { + const theme = useMUITheme(); + + return ( + + + + +

Job Monitor

+ +
+
+
+ ); +} diff --git a/src/components/applications/UserDashboard.tsx b/src/components/applications/UserDashboard.tsx new file mode 100644 index 0000000..4cc07f7 --- /dev/null +++ b/src/components/applications/UserDashboard.tsx @@ -0,0 +1,32 @@ +"use client"; +import * as React from "react"; +import CssBaseline from "@mui/material/CssBaseline"; +import { Box } from "@mui/material"; +import { useMUITheme } from "@/hooks/theme"; +import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles"; +import { useOidcAccessToken } from "@axa-fr/react-oidc"; + +/** + * Build the User Dashboard page + * @returns User Dashboard content + */ +export default function UserDashboard() { + const theme = useMUITheme(); + const { accessTokenPayload } = useOidcAccessToken(); + + return ( + + + + + Hello {accessTokenPayload["preferred_username"]} + + + + ); +} diff --git a/src/components/layout/Dashboard.tsx b/src/components/layout/Dashboard.tsx index 22b13d0..76e8efe 100644 --- a/src/components/layout/Dashboard.tsx +++ b/src/components/layout/Dashboard.tsx @@ -9,6 +9,9 @@ import Toolbar from "@mui/material/Toolbar"; import Stack from "@mui/material/Stack"; import { LoginButton } from "../ui/LoginButton"; import DashboardDrawer from "../ui/DashboardDrawer"; +import { useMUITheme } from "@/hooks/theme"; +import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles"; +import { ThemeToggleButton } from "../ui/ThemeToggleButton"; interface DashboardProps { children: React.ReactNode; @@ -22,6 +25,7 @@ interface DashboardProps { * @return an dashboard layout */ export default function Dashboard(props: DashboardProps) { + const theme = useMUITheme(); /** State management for mobile drawer */ const [mobileOpen, setMobileOpen] = React.useState(false); const handleDrawerToggle = () => { @@ -34,66 +38,70 @@ export default function Dashboard(props: DashboardProps) { return ( - - - - + + + + + + + + - - - - -
- - - - - - {/* Here two types of drawers are rendered: +
+ + + + + + + + + {/* Here two types of drawers are rendered: 1. Temporary drawer: Visible on small screens (xs) and is collapsible. 2. Permanent drawer: Visible on larger screens (sm) and stays fixed. Depending on the screen size, only one will be visible at a time. */} - - - - - {props.children} - + + + + + {props.children} + + ); } diff --git a/src/components/layout/Showcase.tsx b/src/components/layout/Showcase.tsx index 3e6737a..c6090e8 100644 --- a/src/components/layout/Showcase.tsx +++ b/src/components/layout/Showcase.tsx @@ -11,6 +11,9 @@ import { LoginButton } from "../ui/LoginButton"; import { Box, Stack } from "@mui/material"; import { DashboardButton } from "../ui/DashboardButton"; import Image from "next/image"; +import { ThemeToggleButton } from "../ui/ThemeToggleButton"; +import { useMUITheme } from "@/hooks/theme"; +import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles"; /** * Build the showcase content with an AppBar @@ -18,6 +21,8 @@ import Image from "next/image"; * @returns showcase content */ export default function Showcase() { + const theme = useMUITheme(); + const Item = styled(Paper)(({ theme }) => ({ padding: theme.spacing(6), elevation: 0, @@ -25,73 +30,76 @@ export default function Showcase() { return ( - - - - - - - -
- - - - - - - - - - - - - The DIRAC interware is a software framework that enables - communities to interact with distributed computing resources. - DIRAC forms a layer between users and resources, hiding - diversities across computing and storage resources. - - - -
- DIRAC showcase1 -
-
- -
- DIRAC showcase 2 -
-
- - - DIRAC has been adopted by several HEP and non-HEP experiments - communities, with different goals, intents, resources and - workflows: it is experiment agnostic, extensible, and flexible. - + + + + + + + + +
+ + + + + + + + + + + + + + The DIRAC interware is a software framework that enables + communities to interact with distributed computing resources. + DIRAC forms a layer between users and resources, hiding + diversities across computing and storage resources. + + + +
+ DIRAC showcase1 +
+
+ +
+ DIRAC showcase 2 +
+
+ + + DIRAC has been adopted by several HEP and non-HEP experiments + communities, with different goals, intents, resources and + workflows: it is experiment agnostic, extensible, and flexible. + +
- -
+ + ); } diff --git a/src/components/ui/DashboardButton.tsx b/src/components/ui/DashboardButton.tsx index f29ff92..a226b41 100644 --- a/src/components/ui/DashboardButton.tsx +++ b/src/components/ui/DashboardButton.tsx @@ -1,5 +1,6 @@ import { useOidcAccessToken } from "@axa-fr/react-oidc"; import { Button } from "@mui/material"; +import { deepOrange, lightGreen } from "@mui/material/colors"; import Link from "next/link"; /** @@ -14,7 +15,14 @@ export function DashboardButton() { } return ( - ); diff --git a/src/components/data/JobDataGrid.tsx b/src/components/ui/JobDataGrid.tsx similarity index 100% rename from src/components/data/JobDataGrid.tsx rename to src/components/ui/JobDataGrid.tsx diff --git a/src/components/ui/LoginButton.tsx b/src/components/ui/LoginButton.tsx index 4af5ffc..4b6b29a 100644 --- a/src/components/ui/LoginButton.tsx +++ b/src/components/ui/LoginButton.tsx @@ -12,7 +12,6 @@ import { Tooltip, } from "@mui/material"; import { deepOrange, lightGreen } from "@mui/material/colors"; -import NextLink from "next/link"; import React from "react"; /** diff --git a/src/components/ui/ThemeToggleButton.tsx b/src/components/ui/ThemeToggleButton.tsx new file mode 100644 index 0000000..cdd9185 --- /dev/null +++ b/src/components/ui/ThemeToggleButton.tsx @@ -0,0 +1,22 @@ +import { IconButton } from "@mui/material"; +import DarkModeIcon from "@mui/icons-material/DarkMode"; +import LightModeIcon from "@mui/icons-material/LightMode"; +import { useTheme } from "@/hooks/theme"; + +/** + * Toggle button for switching between light and dark themes. + * @returns an IconButton + */ +export function ThemeToggleButton() { + const { theme, toggleTheme } = useTheme(); + + return ( + + {theme === "light" ? ( + + ) : ( + + )} + + ); +} diff --git a/src/contexts/ThemeProvider.tsx b/src/contexts/ThemeProvider.tsx new file mode 100644 index 0000000..074bebe --- /dev/null +++ b/src/contexts/ThemeProvider.tsx @@ -0,0 +1,32 @@ +"use client"; +import { createContext, useState } from "react"; + +type ThemeContextType = { + theme: "light" | "dark"; + toggleTheme: () => void; +}; + +export type ThemeProviderProps = { + children: React.ReactNode; +}; + +export const ThemeContext = createContext( + undefined, +); + +// ThemeProvider component to provide the theme context to its children +export const ThemeProvider = ({ children }: ThemeProviderProps) => { + // State to manage the current theme mode + const [theme, setTheme] = useState<"light" | "dark">("light"); + + // Function to toggle the theme mode + const toggleTheme = () => { + setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light")); + }; + + return ( + + {children} + + ); +}; diff --git a/src/hooks/jobs.tsx b/src/hooks/jobs.tsx index d511273..37f06dd 100644 --- a/src/hooks/jobs.tsx +++ b/src/hooks/jobs.tsx @@ -17,8 +17,11 @@ const fetcher = (args: any[]) => { export function useJobs() { const diracxUrl = useDiracxUrl(); const { accessToken } = useOidcAccessToken(); - const url = `${diracxUrl}/api/jobs/search?page=0&per_page=100`; - const { data, error } = useSWR([url, accessToken], fetcher); + const url = diracxUrl + ? `${diracxUrl}/api/jobs/search?page=0&per_page=100` + : null; + + const { data, error } = useSWR(url ? [url, accessToken] : null, fetcher); if (diracxUrl === null) { return { data: null, error: "diracxUrl is null", isLoading: false }; diff --git a/src/hooks/theme.tsx b/src/hooks/theme.tsx new file mode 100644 index 0000000..9831b2e --- /dev/null +++ b/src/hooks/theme.tsx @@ -0,0 +1,29 @@ +import { ThemeContext } from "@/contexts/ThemeProvider"; +import { createTheme } from "@mui/material/styles"; +import { useContext } from "react"; + +// Custom hook to access the theme context +export const useTheme = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +}; + +// Custom hook to generate and return the Material-UI theme based on the current mode +export const useMUITheme = () => { + const { theme } = useTheme(); + + // Creating a Material-UI theme based on the current mode + const muiTheme = createTheme({ + palette: { + mode: theme, + primary: { + main: "#ffffff", + }, + }, + }); + + return muiTheme; +}; diff --git a/test/DashboardAppBar.test.tsx b/test/DashboardAppBar.test.tsx deleted file mode 100644 index 0c0e6e0..0000000 --- a/test/DashboardAppBar.test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from "react"; -import { render, fireEvent } from "@testing-library/react"; -import Dashboard from "@/components/layout/Dashboard"; -import { useOidc, useOidcAccessToken } from "@axa-fr/react-oidc"; - -// Mock the module -jest.mock("@axa-fr/react-oidc", () => ({ - useOidcAccessToken: jest.fn(), - useOidc: jest.fn(), -})); - -describe("", () => { - beforeEach(() => { - // Mock the return value for each test - (useOidcAccessToken as jest.Mock).mockReturnValue({ - accessTokenPayload: { - test: "test", - }, - }); - (useOidc as jest.Mock).mockReturnValue({ - isAuthenticated: false, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - // Normal case - it("renders on desktop screen", () => { - const { getByTestId } = render( - -

Test

-
, - ); - - // `drawer-temporary` should not even be in the DOM for desktop screen sizes - expect(() => getByTestId("drawer-temporary")).toThrow(); - // Expect `drawer-permanent` to now be visible - expect(getByTestId("drawer-permanent")).toBeVisible(); - }); - - // Testing a hypothetical toggle button for the drawer - it("renders on mobile screen", () => { - global.innerWidth = 350; // e.g., 350px width for mobile - global.dispatchEvent(new Event("resize")); - - const { getByTestId } = render( - -

Test

-
, - ); - const toggleButton = getByTestId("drawer-toggle-button"); - - // Assuming the drawer is initially closed - // `drawer-temporary` should not even be in the DOM initially - expect(() => getByTestId("drawer-temporary")).toThrow(); - - // Simulate a button click - fireEvent.click(toggleButton); - - // Expect the drawer to now be visible - expect(getByTestId("drawer-temporary")).toBeVisible(); - }); -}); diff --git a/test/ThemeToggleButton.test.tsx b/test/ThemeToggleButton.test.tsx new file mode 100644 index 0000000..9472d72 --- /dev/null +++ b/test/ThemeToggleButton.test.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { render, fireEvent } from "@testing-library/react"; +import { ThemeToggleButton } from "@/components/ui/ThemeToggleButton"; +import { useTheme } from "@/hooks/theme"; + +// Mocking the useTheme hook +jest.mock("../src/hooks/theme", () => ({ + useTheme: jest.fn(), +})); + +describe("", () => { + let mockToggleTheme: jest.Mock; + + beforeEach(() => { + mockToggleTheme = jest.fn(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders the DarkModeIcon when theme is "light"', () => { + (useTheme as jest.Mock).mockReturnValue({ + theme: "light", + toggleTheme: mockToggleTheme, + }); + + const { getByTestId, queryByTestId } = render(); + expect(getByTestId("dark-mode")).toBeInTheDocument(); + expect(queryByTestId("light-mode")).not.toBeInTheDocument(); + }); + + it('renders the LightModeIcon when theme is "dark"', () => { + (useTheme as jest.Mock).mockReturnValue({ + theme: "dark", + toggleTheme: mockToggleTheme, + }); + + const { getByTestId, queryByTestId } = render(); + expect(getByTestId("light-mode")).toBeInTheDocument(); + expect(queryByTestId("dark-mode")).not.toBeInTheDocument(); + }); + + it("calls toggleTheme function when button is clicked", () => { + (useTheme as jest.Mock).mockReturnValue({ + theme: "light", + toggleTheme: mockToggleTheme, + }); + + const { getByRole } = render(); + const button = getByRole("button"); + + fireEvent.click(button); + expect(mockToggleTheme).toHaveBeenCalledTimes(1); + }); +}); From 257d0002111c531fdfb8baed2c2f88da32d3413c Mon Sep 17 00:00:00 2001 From: aldbr Date: Mon, 16 Oct 2023 00:39:39 +0900 Subject: [PATCH 2/2] feat: add more tests --- src/contexts/ThemeProvider.tsx | 5 +- test/unit-tests/Dashboard.test.tsx | 70 +++++++++++++++++++ test/unit-tests/DashboardButton.test.tsx | 37 ++++++++++ test/unit-tests/DiracLogo.test.tsx | 22 ++++++ test/unit-tests/JobDataGrid.test.tsx | 48 +++++++++++++ test/unit-tests/LoginButton.test.tsx | 69 ++++++++++++++++++ .../ThemeToggleButton.test.tsx | 2 +- tsconfig.json | 2 +- 8 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 test/unit-tests/Dashboard.test.tsx create mode 100644 test/unit-tests/DashboardButton.test.tsx create mode 100644 test/unit-tests/DiracLogo.test.tsx create mode 100644 test/unit-tests/JobDataGrid.test.tsx create mode 100644 test/unit-tests/LoginButton.test.tsx rename test/{ => unit-tests}/ThemeToggleButton.test.tsx (97%) diff --git a/src/contexts/ThemeProvider.tsx b/src/contexts/ThemeProvider.tsx index 074bebe..f0cf291 100644 --- a/src/contexts/ThemeProvider.tsx +++ b/src/contexts/ThemeProvider.tsx @@ -1,4 +1,5 @@ "use client"; +import { useMediaQuery } from "@mui/material"; import { createContext, useState } from "react"; type ThemeContextType = { @@ -17,7 +18,9 @@ export const ThemeContext = createContext( // ThemeProvider component to provide the theme context to its children export const ThemeProvider = ({ children }: ThemeProviderProps) => { // State to manage the current theme mode - const [theme, setTheme] = useState<"light" | "dark">("light"); + const [theme, setTheme] = useState<"light" | "dark">( + useMediaQuery("(prefers-color-scheme: dark)") ? "dark" : "light", + ); // Function to toggle the theme mode const toggleTheme = () => { diff --git a/test/unit-tests/Dashboard.test.tsx b/test/unit-tests/Dashboard.test.tsx new file mode 100644 index 0000000..542aece --- /dev/null +++ b/test/unit-tests/Dashboard.test.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import { render, fireEvent } from "@testing-library/react"; +import Dashboard from "@/components/layout/Dashboard"; +import { ThemeProvider } from "@/contexts/ThemeProvider"; +import { useOidc, useOidcAccessToken } from "@axa-fr/react-oidc"; + +// Mock the module +jest.mock("@axa-fr/react-oidc", () => ({ + useOidcAccessToken: jest.fn(), + useOidc: jest.fn(), +})); + +describe("", () => { + beforeEach(() => { + // Mock the return value for each test + (useOidcAccessToken as jest.Mock).mockReturnValue({ + accessTokenPayload: { + test: "test", + }, + }); + (useOidc as jest.Mock).mockReturnValue({ + isAuthenticated: false, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + // Normal case + it("renders on desktop screen", () => { + const { getByTestId } = render( + + +

Test

+
+
, + ); + + // `drawer-temporary` should not even be in the DOM for desktop screen sizes + expect(() => getByTestId("drawer-temporary")).toThrow(); + // Expect `drawer-permanent` to now be visible + expect(getByTestId("drawer-permanent")).toBeVisible(); + }); + + // Testing a hypothetical toggle button for the drawer + it("renders on mobile screen", () => { + global.innerWidth = 350; // e.g., 350px width for mobile + global.dispatchEvent(new Event("resize")); + + const { getByTestId } = render( + + +

Test

+
+
, + ); + const toggleButton = getByTestId("drawer-toggle-button"); + + // Assuming the drawer is initially closed + // `drawer-temporary` should not even be in the DOM initially + expect(() => getByTestId("drawer-temporary")).toThrow(); + + // Simulate a button click + fireEvent.click(toggleButton); + + // Expect the drawer to now be visible + expect(getByTestId("drawer-temporary")).toBeVisible(); + }); +}); diff --git a/test/unit-tests/DashboardButton.test.tsx b/test/unit-tests/DashboardButton.test.tsx new file mode 100644 index 0000000..e16f40a --- /dev/null +++ b/test/unit-tests/DashboardButton.test.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { DashboardButton } from "@/components/ui/DashboardButton"; +import { useOidcAccessToken } from "@axa-fr/react-oidc"; + +// Mocking the useOidcAccessToken hook +jest.mock("@axa-fr/react-oidc", () => ({ + useOidcAccessToken: jest.fn(), +})); + +describe("", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("renders the button when user is connected (has accessToken)", () => { + // Mocking the return value of useOidcAccessToken to simulate a user with an accessToken + (useOidcAccessToken as jest.Mock).mockReturnValue({ + accessToken: "mocked_token", + }); + + const { getByText } = render(); + const button = getByText("Dashboard"); + + expect(button).toBeInTheDocument(); + }); + + it("does not render the button when user is not connected (no accessToken)", () => { + // Mocking the return value of useOidcAccessToken to simulate a user without an accessToken + (useOidcAccessToken as jest.Mock).mockReturnValue({ accessToken: null }); + + const { queryByText } = render(); + const button = queryByText("Dashboard"); + + expect(button).not.toBeInTheDocument(); + }); +}); diff --git a/test/unit-tests/DiracLogo.test.tsx b/test/unit-tests/DiracLogo.test.tsx new file mode 100644 index 0000000..9f2a998 --- /dev/null +++ b/test/unit-tests/DiracLogo.test.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { DiracLogo } from "@/components/ui/DiracLogo"; + +describe("", () => { + it("renders the logo image with correct attributes", () => { + const { getByAltText } = render(); + + // Check if the image is rendered with the correct alt text + const logoImage = getByAltText("DIRAC logo"); + expect(logoImage).toBeInTheDocument(); + }); + + it("renders the link that redirects to the root page", () => { + const { getByRole } = render(); + + // Check if the link is rendered and points to the root page + const link = getByRole("link"); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute("href", "/"); + }); +}); diff --git a/test/unit-tests/JobDataGrid.test.tsx b/test/unit-tests/JobDataGrid.test.tsx new file mode 100644 index 0000000..6784d3f --- /dev/null +++ b/test/unit-tests/JobDataGrid.test.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { JobDataGrid } from "@/components/ui/JobDataGrid"; +import { useJobs } from "@/hooks/jobs"; + +// Mocking the useJobs hook +jest.mock("../../src/hooks/jobs"); + +describe("", () => { + it("displays loading state", () => { + (useJobs as jest.Mock).mockReturnValue({ isLoading: true }); + + const { getByText } = render(); + expect(getByText("Loading...")).toBeInTheDocument(); + }); + + it("displays error state", () => { + (useJobs as jest.Mock).mockReturnValue({ error: true }); + + const { getByText } = render(); + expect( + getByText("An error occurred while fetching jobs."), + ).toBeInTheDocument(); + }); + + it("displays no jobs data state", () => { + (useJobs as jest.Mock).mockReturnValue({ data: [] }); + + const { getByText } = render(); + expect(getByText("No job submitted.")).toBeInTheDocument(); + }); + + it("displays jobs data in the grid", () => { + const mockData = [ + { + JobID: "1", + JobName: "TestJob1", + Status: "Running", + MinorStatus: "Processing", + SubmissionTime: "2023-10-13", + }, + ]; + (useJobs as jest.Mock).mockReturnValue({ data: mockData }); + + const { getByText } = render(); + expect(getByText("TestJob1")).toBeInTheDocument(); + }); +}); diff --git a/test/unit-tests/LoginButton.test.tsx b/test/unit-tests/LoginButton.test.tsx new file mode 100644 index 0000000..0efcdf3 --- /dev/null +++ b/test/unit-tests/LoginButton.test.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { render, fireEvent } from "@testing-library/react"; +import { LoginButton } from "@/components/ui/LoginButton"; +import { useOidcAccessToken, useOidc } from "@axa-fr/react-oidc"; + +// Mocking the hooks +jest.mock("@axa-fr/react-oidc"); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe("", () => { + it('displays the "Login" button when not authenticated', () => { + (useOidc as jest.Mock).mockReturnValue({ isAuthenticated: false }); + (useOidcAccessToken as jest.Mock).mockReturnValue({}); + + const { getByText } = render(); + expect(getByText("Login")).toBeInTheDocument(); + }); + + it("displays the user avatar when authenticated", () => { + (useOidc as jest.Mock).mockReturnValue({ isAuthenticated: true }); + (useOidcAccessToken as jest.Mock).mockReturnValue({ + accessToken: "mockAccessToken", + accessTokenPayload: { preferred_username: "John" }, + }); + + const { getByText } = render(); + expect(getByText("J")).toBeInTheDocument(); // Assuming 'John' is the preferred username and 'J' is the first letter. + }); + + it("opens the menu when avatar is clicked", () => { + (useOidc as jest.Mock).mockReturnValue({ isAuthenticated: true }); + (useOidcAccessToken as jest.Mock).mockReturnValue({ + accessToken: "mockAccessToken", + accessTokenPayload: { preferred_username: "John" }, + }); + + const { getByText, queryByText } = render(); + fireEvent.click(getByText("J")); + expect(queryByText("Profile")).toBeInTheDocument(); + expect(queryByText("Logout")).toBeInTheDocument(); + }); + + it('calls the logout function when "Logout" is clicked', () => { + const mockLogout = jest.fn(); + + (useOidc as jest.Mock).mockReturnValue({ + isAuthenticated: true, + logout: mockLogout, + }); + (useOidcAccessToken as jest.Mock).mockReturnValue({ + accessToken: "mockAccessToken", + accessTokenPayload: { preferred_username: "John" }, + }); + + const { getByText } = render(); + + // Open the menu by clicking the avatar + fireEvent.click(getByText("J")); + + // Click the "Logout" option + fireEvent.click(getByText("Logout")); + + // Ensure the mockLogout function was called + expect(mockLogout).toHaveBeenCalled(); + }); +}); diff --git a/test/ThemeToggleButton.test.tsx b/test/unit-tests/ThemeToggleButton.test.tsx similarity index 97% rename from test/ThemeToggleButton.test.tsx rename to test/unit-tests/ThemeToggleButton.test.tsx index 9472d72..faea825 100644 --- a/test/ThemeToggleButton.test.tsx +++ b/test/unit-tests/ThemeToggleButton.test.tsx @@ -4,7 +4,7 @@ import { ThemeToggleButton } from "@/components/ui/ThemeToggleButton"; import { useTheme } from "@/hooks/theme"; // Mocking the useTheme hook -jest.mock("../src/hooks/theme", () => ({ +jest.mock("../../src/hooks/theme", () => ({ useTheme: jest.fn(), })); diff --git a/tsconfig.json b/tsconfig.json index 0914988..be05ba1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,5 +24,5 @@ } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules", "test/*.ts", "test/*.tsx", "jest.setup.ts"] + "exclude": ["node_modules", "test/*/*.ts", "test/*/*.tsx", "jest.setup.ts"] }