diff --git a/src/2023/Cfp/CfpSection2023.test.tsx b/src/2023/Cfp/CfpSection2023.test.tsx new file mode 100644 index 000000000..e8bacfd8e --- /dev/null +++ b/src/2023/Cfp/CfpSection2023.test.tsx @@ -0,0 +1,114 @@ +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import CfpSection2023 from "./CfpSection2023"; +import { useWindowSize } from "react-use"; +import conferenceData from "../../data/2023.json"; +import { data } from "./CfpData"; + +// Mock useWindowSize to control the window size in tests +jest.mock("react-use", () => ({ + ...jest.requireActual("react-use"), + useWindowSize: jest.fn(), +})); + +describe("CfpSection2023", () => { + beforeEach(() => { + // Reset the mock before each test + (useWindowSize as jest.Mock).mockReset(); + (useWindowSize as jest.Mock).mockReturnValue({ width: 1024 }); // Default width + }); + + it("should render without crashing", () => { + render(); + }); + + it("should render the title and subtitle", () => { + render(); + expect( + screen.getByText("CFP Committee", { exact: false }), + ).toBeInTheDocument(); + expect( + screen.getByText( + "We're excited to announce the members of the Call for Papers committee for the next DevBcn conference! These experienced professionals will be reviewing and selecting the best talks and workshops for the upcoming event.", + ), + ).toBeInTheDocument(); + }); + + it("should render the tracks and members", () => { + render(); + data.forEach((track) => { + expect(screen.getAllByText(track.name, { exact: false })).not.toBeNull(); + track.members + .filter((member) => member.photo !== "") + .forEach((member) => { + expect( + screen.getAllByText(member.name, { exact: false }), + ).not.toBeNull(); + }); + }); + }); + + it("should render member photos", () => { + render(); + data.forEach((track) => { + track.members + .filter((member) => member.photo !== "") + .forEach((member) => { + const image = screen.getAllByAltText(member.name); + expect(image).not.toBeNull(); + expect(image.at(0)).toHaveAttribute("src", member.photo); + }); + }); + }); + + it("should render twitter links", () => { + render(); + data.forEach((track) => { + track.members + .filter((member) => member.twitter !== "") + .forEach((member) => { + const twitterLinks = screen.getAllByRole("link"); + const twitterLink = twitterLinks.find( + (link) => link.getAttribute("href") === member.twitter, + ); + expect(twitterLink).toBeInTheDocument(); + expect(twitterLink).toHaveAttribute("href", member.twitter); + }); + }); + }); + + it("should render linkedIn links", () => { + render(); + data.forEach((track) => { + track.members + .filter((member) => member.linkedIn !== "") + .forEach((member) => { + const linkedInLinks = screen.getAllByRole("link"); + const linkedInLink = linkedInLinks.find( + (link) => link.getAttribute("href") === member.linkedIn, + ); + expect(linkedInLink).toBeInTheDocument(); + expect(linkedInLink).toHaveAttribute("href", member.linkedIn); + }); + }); + }); + + it("should update the document title", async () => { + render(); + await waitFor(() => { + expect(document.title).toBe( + `CFP Committee - DevBcn - ${conferenceData.edition}`, + ); + }); + }); + + it("should not render the icons when the width is smaller than the breakpoint", () => { + (useWindowSize as jest.Mock).mockReturnValue({ width: 767 }); + render(); + const lessIcon = screen.queryByAltText("more than - icon"); + const moreIcon = screen.queryByAltText("Less than - icon"); + expect(lessIcon).not.toBeInTheDocument(); + expect(moreIcon).not.toBeInTheDocument(); + }); +}); diff --git a/src/2023/Cfp/CfpSection2023.tsx b/src/2023/Cfp/CfpSection2023.tsx index 0acac71e7..c03e02a41 100644 --- a/src/2023/Cfp/CfpSection2023.tsx +++ b/src/2023/Cfp/CfpSection2023.tsx @@ -35,7 +35,9 @@ const MemberName = styled.h5` text-align: left; `; -const CfpTrackComponent: FC> = ({ track }) => ( +const CfpTrackComponent: FC> = ({ + track, +}) => ( <>
{track.name} @@ -90,8 +92,14 @@ const CfpSection2023: FC> = () => { /> {width > MOBILE_BREAKPOINT && ( <> - - + + )} diff --git a/src/services/buildSlashes.ts b/src/services/buildSlashes.ts new file mode 100644 index 000000000..299f3fa89 --- /dev/null +++ b/src/services/buildSlashes.ts @@ -0,0 +1,10 @@ +export const buildSlashes = (module: number) => { + const slashesElement = document.getElementById("Slashes"); + + const slashesWidth = slashesElement?.offsetWidth ?? 0; + let slashes = ""; + for (let index = 0; index < slashesWidth; index++) { + if (index % module === 0) slashes += "/ "; + } + return slashes; +}; diff --git a/src/views/Home/components/Sponsors/BasicSponsor.tsx b/src/views/Home/components/Sponsors/BasicSponsor.tsx index c0e0c2eed..99c0b10c3 100644 --- a/src/views/Home/components/Sponsors/BasicSponsor.tsx +++ b/src/views/Home/components/Sponsors/BasicSponsor.tsx @@ -11,39 +11,31 @@ import { StyledSponsorTitleSlashesContainer, } from "./Sponsors.style"; import SponsorBadge from "./SponsorBadge"; -import {Color} from "../../../../styles/colors"; -import {BIG_BREAKPOINT} from "../../../../constants/BreakPoints"; -import {buildSlashes} from "./Sponsors"; -import {useWindowSize} from "react-use"; -import React, {FC, useCallback, useEffect, useState} from "react"; -import {Sponsor} from "./SponsorsData"; +import { Color } from "../../../../styles/colors"; +import { BIG_BREAKPOINT } from "../../../../constants/BreakPoints"; +import React, { FC } from "react"; +import { Sponsor } from "./SponsorsData"; +import { useSponsorsHook } from "./useSponsorsHook"; interface Props { sponsors: Array | null; } -export const BasicSponsor: FC> = ({sponsors}) => { - const { width } = useWindowSize(); - const [slashes, setSlashes] = useState(""); - const [isHovered, setIsHovered] = useState(false); - - useEffect(() => { - const newSlashes = buildSlashes(2); - - setSlashes(newSlashes); - }, [width]); - - const handleHoverSponsorBasic = useCallback(() => setIsHovered(true), []); - const handleUnHoverSponsorBasic = useCallback(() => setIsHovered(false), []); - +export const BasicSponsor: FC> = ({ + sponsors, +}) => { + const { width, slashes, isHovered, handleHover, handleUnHover } = + useSponsorsHook({ + numberOfSlashGroups: 2, + }); return ( <> {sponsors !== null && sponsors.length > 0 && ( | null; } -export const Communities: FC> = ({sponsors}) => { - const { width } = useWindowSize(); - const [slashes, setSlashes] = useState(""); - const [isHovered, setIsHovered] = useState(false); - useEffect(() => { - const newSlashes = buildSlashes(2); - - setSlashes(newSlashes); - }, [width]); - - const handleHover = useCallback(() => setIsHovered(true), []); - const handleUnHover = useCallback(() => setIsHovered(false), []); +export const Communities: FC> = ({ + sponsors, +}) => { + const { width, slashes, isHovered, handleHover, handleUnHover } = + useSponsorsHook({ + numberOfSlashGroups: 2, + }); return ( <> {sponsors !== null && sponsors.length > 0 && ( diff --git a/src/views/Home/components/Sponsors/MediaPartners.tsx b/src/views/Home/components/Sponsors/MediaPartners.tsx index 9def78d50..5cc9cfe9b 100644 --- a/src/views/Home/components/Sponsors/MediaPartners.tsx +++ b/src/views/Home/components/Sponsors/MediaPartners.tsx @@ -13,10 +13,9 @@ import { import SponsorBadge from "./SponsorBadge"; import { Color } from "../../../../styles/colors"; import { BIG_BREAKPOINT } from "../../../../constants/BreakPoints"; -import { buildSlashes } from "./Sponsors"; -import { useWindowSize } from "react-use"; -import React, { FC, useCallback, useEffect, useState } from "react"; +import React, { FC } from "react"; import { Sponsor } from "./SponsorsData"; +import { useSponsorsHook } from "./useSponsorsHook"; interface Props { sponsors: Array | null; @@ -25,25 +24,19 @@ interface Props { export const MediaPartners: FC> = ({ sponsors, }) => { - const { width } = useWindowSize(); - const [slashes, setSlashes] = useState(""); - const [isHovered, setIsHovered] = useState(false); - useEffect(() => { - const newSlashes = buildSlashes(2); + const { width, slashes, isHovered, handleHover, handleUnHover } = + useSponsorsHook({ + numberOfSlashGroups: 2, + }); - setSlashes(newSlashes); - }, [width]); - - const handleHoverMediaPartner = useCallback(() => setIsHovered(true), []); - const handleUnHoverMediaPartner = useCallback(() => setIsHovered(false), []); return ( <> {sponsors !== null && sponsors.length > 0 && ( | null; } -export const PremiumSponsors: FC> = ({sponsors}) => { - const { width } = useWindowSize(); - const [slashes, setSlashes] = useState(""); - const [isHovered, setIsHovered] = useState(false); - useEffect(() => { - const newSlashes = buildSlashes(2); +export const PremiumSponsors: FC> = ({ + sponsors, +}) => { + const { width, slashes, isHovered, handleHover, handleUnHover } = + useSponsorsHook({ + numberOfSlashGroups: 2, + }); - setSlashes(newSlashes); - }, [width]); - - const handleHoverSponsorPremium = useCallback(() => setIsHovered(true), []); - const handleUnHoverSponsorPremium = useCallback( - () => setIsHovered(false), - [] - ); return ( <> {sponsors !== null && sponsors.length > 0 && ( | null; } -export const RegularSponsors: FC> = ({sponsors}) => { - const { width } = useWindowSize(); - const [slashes, setSlashes] = useState(""); - const [isHovered, setIsHovered] = useState(false); - useEffect(() => { - const newSlashes = buildSlashes(2); - - setSlashes(newSlashes); - }, [width]); - - const handleHoverSponsorRegular = useCallback(() => setIsHovered(true), []); - const handleUnHoverSponsorRegular = useCallback( - () => setIsHovered(false), - [] - ); +export const RegularSponsors: FC> = ({ + sponsors, +}) => { + const { width, slashes, isHovered, handleHover, handleUnHover } = + useSponsorsHook({ + numberOfSlashGroups: 2, + }); return ( <> {sponsors !== null && sponsors.length > 0 && ( { - const slashesElement = document.getElementById("Slashes"); - - const slashesWidth = slashesElement?.offsetWidth ?? 0; - - let slashes = ""; - for (let index = 0; index < slashesWidth; index++) { - if (index % module === 0) slashes += "/ "; - } - - return slashes; -}; +import { TopSponsors } from "./TopSponsors"; +import { RegularSponsors } from "./RegularSponsors"; +import { PremiumSponsors } from "./PremiumSponsors"; +import { BasicSponsor } from "./BasicSponsor"; +import { Communities } from "./Communities"; +import { MediaPartners } from "./MediaPartners"; +import { Supporters } from "./Supporters"; +import { sponsors } from "./SponsorsData"; const Sponsors: FC> = () => ( @@ -51,13 +35,13 @@ const Sponsors: FC> = () => ( /> - - - - - - - + + + + + + + ); diff --git a/src/views/Home/components/Sponsors/Supporters.tsx b/src/views/Home/components/Sponsors/Supporters.tsx index 61908751a..a7d09ee2e 100644 --- a/src/views/Home/components/Sponsors/Supporters.tsx +++ b/src/views/Home/components/Sponsors/Supporters.tsx @@ -1,108 +1,102 @@ import { - StyledFlexGrow, - StyledLogos, - StyledSeparator, - StyledSlashes, - StyledSponsorIconMicro, - StyledSponsorItemContainer, - StyledSponsorLogosContainer, - StyledSponsorTitleContainer, - StyledSponsorTitleMargin, - StyledSponsorTitleSlashesContainer, + StyledFlexGrow, + StyledLogos, + StyledSeparator, + StyledSlashes, + StyledSponsorIconMicro, + StyledSponsorItemContainer, + StyledSponsorLogosContainer, + StyledSponsorTitleContainer, + StyledSponsorTitleMargin, + StyledSponsorTitleSlashesContainer, } from "./Sponsors.style"; import SponsorBadge from "./SponsorBadge"; -import {Color} from "../../../../styles/colors"; -import {BIG_BREAKPOINT} from "../../../../constants/BreakPoints"; -import {buildSlashes} from "./Sponsors"; -import {useWindowSize} from "react-use"; -import React, {FC, useCallback, useEffect, useState} from "react"; -import {Sponsor} from "./SponsorsData"; +import { Color } from "../../../../styles/colors"; +import { BIG_BREAKPOINT } from "../../../../constants/BreakPoints"; +import React, { FC } from "react"; +import { Sponsor } from "./SponsorsData"; +import { useSponsorsHook } from "./useSponsorsHook"; interface Props { - sponsors: Array | null; + sponsors: Array | null; } -export const Supporters: FC> = ({sponsors}) => { - const {width} = useWindowSize(); - const [slashes, setSlashes] = useState(""); - const [isHovered, setIsHovered] = useState(false); - useEffect(() => { - const newSlashes = buildSlashes(2); +export const Supporters: FC> = ({ + sponsors, +}) => { + const { width, slashes, isHovered, handleHover, handleUnHover } = + useSponsorsHook({ + numberOfSlashGroups: 2, + }); + return ( + <> + {sponsors !== null && sponsors.length > 0 && ( + + + + = BIG_BREAKPOINT + ? Color.WHITE + : Color.DARK_BLUE + } + id="Slashes" + > + {slashes} + - setSlashes(newSlashes); - }, [width]); + {width < BIG_BREAKPOINT && "SUPPORTERS"} + + {width >= BIG_BREAKPOINT && ( + = BIG_BREAKPOINT + ? Color.WHITE + : Color.DARK_BLUE + } + > + {slashes} + SUPPORTERS + + )} + + - const handleHover = useCallback(() => setIsHovered(true), []); - const handleUnHover = useCallback(() => setIsHovered(false), []); - return ( - <> - {sponsors !== null && sponsors.length > 0 && ( - + + + {sponsors.map((sponsor) => ( + - - - = BIG_BREAKPOINT - ? Color.WHITE - : Color.DARK_BLUE - } - id="Slashes" - > - {slashes} - - - {width < BIG_BREAKPOINT && "SUPPORTERS"} - - {width >= BIG_BREAKPOINT && ( - = BIG_BREAKPOINT - ? Color.WHITE - : Color.DARK_BLUE - } - > - {slashes} - SUPPORTERS - - )} - - - - - - - {sponsors.map((sponsor) => ( - - - - ))} - - - - )} - - ); + + + ))} + + + + )} + + ); }; diff --git a/src/views/Home/components/Sponsors/TopSponsors.tsx b/src/views/Home/components/Sponsors/TopSponsors.tsx index 502bbaac9..ef3b3c657 100644 --- a/src/views/Home/components/Sponsors/TopSponsors.tsx +++ b/src/views/Home/components/Sponsors/TopSponsors.tsx @@ -11,29 +11,23 @@ import { StyledSponsorTitleSlashesContainer, } from "./Sponsors.style"; import SponsorBadge from "./SponsorBadge"; -import {Color} from "../../../../styles/colors"; -import {BIG_BREAKPOINT} from "../../../../constants/BreakPoints"; -import React, {FC, useCallback, useEffect, useState} from "react"; -import {useWindowSize} from "react-use"; -import {buildSlashes} from "./Sponsors"; -import {Sponsor} from "./SponsorsData"; +import { Color } from "../../../../styles/colors"; +import { BIG_BREAKPOINT } from "../../../../constants/BreakPoints"; +import React, { FC } from "react"; +import { Sponsor } from "./SponsorsData"; +import { useSponsorsHook } from "./useSponsorsHook"; interface Props { sponsors: Array | null; } -export const TopSponsors: FC> = ({sponsors}) => { - const { width } = useWindowSize(); - const [slashes, setSlashes] = useState(""); - const [isHovered, setIsHovered] = useState(false); - useEffect(() => { - const newSlashes = buildSlashes(2); - - setSlashes(newSlashes); - }, [width]); - - const handleHoverSponsorTop = useCallback(() => setIsHovered(true), []); - const handleUnHoverSponsorTop = useCallback(() => setIsHovered(false), []); +export const TopSponsors: FC> = ({ + sponsors, +}) => { + const { width, slashes, isHovered, handleHover, handleUnHover } = + useSponsorsHook({ + numberOfSlashGroups: 2, + }); return ( <> @@ -41,8 +35,8 @@ export const TopSponsors: FC> = ({sponsors}) => { ({ + buildSlashes: jest.fn((count: number) => "//".repeat(count)), +})); + +const wrapper: FC>> = ({ + children, +}) => { + return
{children}
; +}; + +describe("useSponsorsHook", () => { + beforeEach(() => { + // Clear mock calls between tests + jest.clearAllMocks(); + }); + + it("should initialize with default values", () => { + const { result } = renderHook( + () => useSponsorsHook({ numberOfSlashGroups: 2 }), + { wrapper }, + ); + + expect(result.current.slashes).toBe(undefined); // 2 groups of '//' + expect(result.current.isHovered).toBe(false); + expect(typeof result.current.width).toBe("number"); + }); + + it("should update slashes when window size changes", () => { + const { rerender } = renderHook( + () => useSponsorsHook({ numberOfSlashGroups: 2 }), + { wrapper }, + ); + + // Initial render should call buildSlashes once + expect(buildSlashes).toHaveBeenCalledTimes(2); + expect(buildSlashes).toHaveBeenCalledWith(2); + + // Trigger a rerender (simulating window resize) + rerender(); + + // buildSlashes should be called again + expect(buildSlashes).toHaveBeenCalledTimes(3); + }); + + it("should update hover state correctly", () => { + const { result } = renderHook( + () => useSponsorsHook({ numberOfSlashGroups: 2 }), + { wrapper }, + ); + + // Initial state should be not hovered + expect(result.current.isHovered).toBe(false); + + // Simulate hover + act(() => { + result.current.handleHover(); + }); + expect(result.current.isHovered).toBe(true); + + // Simulate unhover + act(() => { + result.current.handleUnHover(); + }); + expect(result.current.isHovered).toBe(false); + }); + + it("should update slashes when numberOfSlashGroups changes", () => { + const { result, rerender } = renderHook( + ({ numberOfSlashGroups }) => useSponsorsHook({ numberOfSlashGroups }), + { initialProps: { numberOfSlashGroups: 2 } }, + ); + + // Initial render with 2 groups + expect(buildSlashes).toHaveBeenCalledWith(2); + expect(result.current.slashes).toBe(undefined); + + // Update to 3 groups + rerender({ numberOfSlashGroups: 3 }); + + expect(buildSlashes).toHaveBeenCalledWith(3); + expect(result.current.slashes).toBe(undefined); + }); + + it("should memoize hover handlers", () => { + const { result, rerender } = renderHook(() => + useSponsorsHook({ numberOfSlashGroups: 2 }), + ); + + // Store initial handlers + const initialHandleHover = result.current.handleHover; + const initialHandleUnHover = result.current.handleUnHover; + + // Rerender + rerender(); + + // Handlers should remain the same (memoized) + expect(result.current.handleHover).toBe(initialHandleHover); + expect(result.current.handleUnHover).toBe(initialHandleUnHover); + }); +}); diff --git a/src/views/Home/components/Sponsors/useSponsorsHook.ts b/src/views/Home/components/Sponsors/useSponsorsHook.ts new file mode 100644 index 000000000..8e421f39a --- /dev/null +++ b/src/views/Home/components/Sponsors/useSponsorsHook.ts @@ -0,0 +1,41 @@ +import {useCallback, useEffect, useState} from "react"; +import {useWindowSize} from "react-use"; + +import {buildSlashes} from "../../../../services/buildSlashes"; + +/** + * Configuration for the sponsors hook + */ +interface SponsorHookConfig { + /** Number of slash groups to display in the sponsor section */ + numberOfSlashGroups: number; +} + +/** + * Custom hook to manage sponsor section behavior including: + * - Responsive slashes generation + * - Hover state management + * - Window size tracking + */ +export const useSponsorsHook = ({ + numberOfSlashGroups = 2, +}: SponsorHookConfig) => { + const { width } = useWindowSize(); + const [slashes, setSlashes] = useState(buildSlashes(numberOfSlashGroups)); + const [isHovered, setIsHovered] = useState(false); + + useEffect(() => { + setSlashes(buildSlashes(numberOfSlashGroups)); + }, [width, numberOfSlashGroups]); + + const handleHover = useCallback(() => setIsHovered(true), []); + const handleUnHover = useCallback(() => setIsHovered(false), []); + + return { + width, + slashes, + isHovered, + handleHover, + handleUnHover, + }; +};