Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ sonar.language=ts

sonar.coverage.exclusions=**/*.test.tsx,**/*.test.ts,**/*.spec.tsx,**/*.spec.ts,**/__tests__/**/*,**/test/**/*,**/tests/**/*,**/*.stories.tsx,**/*.stories.ts,**/index.ts,**/index.tsx

sonar.exclusions=**/node_modules/**,**/dist/**,**/build/**,**/.next/**,**/coverage/**,**/.git/**,**/*.config.js,**/*.config.ts,**/vite.config.ts,**/tsconfig*.json,**/package*.json,**/public/**,**/assets/**,**/*.d.ts,**/.storybook/**
sonar.exclusions=**/node_modules/**,**/dist/**,**/build/**,**/.next/**,**/coverage/**,**/.git/**,**/*.config.js,**/*.config.ts,**/vite.config.ts,**/tsconfig*.json,**/package*.json,**/public/**,**/assets/**,**/utils/**,**/*.d.ts,**/.storybook/**
sonar.test.inclusions=**/*.test.tsx,**/*.test.ts,**/*.spec.tsx,**/*.spec.ts,**/__tests__/**/*,**/test/**/*,**/tests/**/*
sonar.cpd.ts.minimumTokens=100
sonar.cpd.tsx.minimumTokens=100
Expand Down
56 changes: 56 additions & 0 deletions frontend/src/components/atoms/Card/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect, beforeEach } from "vitest";
import Card from "./index";
import { theme } from "../../../styles/theme";
import { ThemeProvider } from "styled-components";
import { DefaultTheme } from "styled-components/dist/types";

const mockTheme = {
colors: {
textPrimary: "red",
accent: "blue",
},
transitions: {
base: "all 0.3s ease",
},
} as unknown as DefaultTheme;
const renderWithTheme = (iconUIComp: React.ReactNode) => {
return render(<ThemeProvider theme={mockTheme}>{iconUIComp}</ThemeProvider>);
};

describe("Card Component", () => {
beforeEach(() => {
(theme.colors.backgroundAlt ) = "#ededed";
});

it("should renders children correctly", () => {
renderWithTheme(<Card>Test Content</Card>);
expect(screen.getByText("Test Content")).toBeInTheDocument();
});

it("should applies default theme background color", () => {
renderWithTheme(<Card>Test</Card>);
const cardElement = screen.getByText("Test");
expect(cardElement).toHaveStyle(`background-color: ${theme.colors.backgroundAlt}`);
});

it("should applies fallback background color when theme.colors.backgroundAlt is missing", () => {
(theme.colors.backgroundAlt as unknown) = undefined;
renderWithTheme(<Card>Fallback Test</Card>);
const cardElement = screen.getByText("Fallback Test");
expect(cardElement).toHaveStyle("background-color: #f9f9f9");
});

it("should applies custom style overrides", () => {
renderWithTheme(
<Card style={{ backgroundColor: "red", padding: "2rem" }}>
Custom Style
</Card>
);
const cardElement = screen.getByText("Custom Style");
expect(cardElement).toHaveStyle("background-color: rgb(255, 0, 0)");

expect(cardElement).toHaveStyle("padding: 2rem");
});
});
56 changes: 56 additions & 0 deletions frontend/src/components/atoms/Icon/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import Icon from "./index";
import { FaBeer } from "react-icons/fa";
import { ThemeProvider } from "styled-components";
import { describe, expect, it, vi } from "vitest";
import { DefaultTheme } from "styled-components/dist/types";
import { STYLED_ICON_TEST_ID } from "../../../utils/constants";
const mockTheme = {
colors: {
textPrimary: "black",
accent: "blue",
},
transitions: {
base: "all 0.3s ease",
},
} as unknown as DefaultTheme;
// βœ… Utility to wrap components with ThemeProvider
const renderWithTheme = (iconUIComp: React.ReactNode) => {
return render(<ThemeProvider theme={mockTheme}>{iconUIComp}</ThemeProvider>);
};

describe("Icon component", () => {
it("should render the passed icon component", () => {
renderWithTheme(<Icon icon={FaBeer} />);
const svgElement = screen.getByTestId(STYLED_ICON_TEST_ID).querySelector("svg");
expect(svgElement).toBeInTheDocument();
});

it("should apply size and color props to the SVG", () => {
renderWithTheme(<Icon icon={FaBeer} size="32px" color="red" />);
const svgElement = screen.getByTestId(STYLED_ICON_TEST_ID).querySelector("svg")!;
expect(svgElement).toHaveStyle({
width: "32px",
height: "32px",
color: "rgb(255, 0, 0)",
});
});

it("should call onClick when clicked", () => {
const handleClick = vi.fn();
renderWithTheme(<Icon icon={FaBeer} onClick={handleClick} />);
fireEvent.click(screen.getByTestId(STYLED_ICON_TEST_ID));
expect(handleClick).toHaveBeenCalledTimes(1);
});

it("should render without optional props using theme defaults", () => {
renderWithTheme(<Icon icon={FaBeer} />);
const svgElement = screen.getByTestId(STYLED_ICON_TEST_ID).querySelector("svg");
expect(svgElement).toHaveStyle({
width: "24px",
height: "24px",
color: "rgb(0, 0, 0)",
});
});
});
3 changes: 2 additions & 1 deletion frontend/src/components/atoms/Icon/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { IconType } from "react-icons";
import { StyledIcon } from "./index.styles";
import { STYLED_ICON_TEST_ID } from "../../../utils/constants";

interface IIcon {
icon: IconType;
Expand All @@ -11,7 +12,7 @@ interface IIcon {

const Icon: React.FC<IIcon> = ({ icon: IconComp, size, color, onClick }) => {
return (
<StyledIcon size={size} color={color} onClick={onClick}>
<StyledIcon size={size} color={color} onClick={onClick} data-testid={STYLED_ICON_TEST_ID}>
<IconComp />
</StyledIcon>
);
Expand Down
74 changes: 74 additions & 0 deletions frontend/src/components/atoms/Image/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Image from "./index";
import { ThemeProvider } from "styled-components";
import { theme } from "../../../styles/theme";
import { OVERLAY_TEST_ID, OVERLAY_TEXT, IMAGE_WRAPPER_TEST_ID } from "../../../utils/constants";

const renderWithTheme = (iconUIComp: React.ReactNode) => {
return render(<ThemeProvider theme={theme}>{iconUIComp}</ThemeProvider>);
};

describe("Image component", () => {
it("renders the image with src and alt", () => {
renderWithTheme(<Image src="test.jpg" alt="Test image" />);
const image = screen.getByAltText("Test image");
expect(image).toBeInTheDocument();
expect(image).toHaveAttribute("src", "test.jpg");
});

it("renders overlay and overlay content when props provided", () => {
renderWithTheme(
<Image
src="test.jpg"
alt="Test image"
overlay
overlayContent={<span>Overlay Text</span>}
/>
);
expect(screen.getByTestId(OVERLAY_TEST_ID)).toBeInTheDocument();
expect(screen.getByText(OVERLAY_TEXT)).toBeInTheDocument();
});

it("applies styles for width, height, rounded, circle", () => {
renderWithTheme(
<Image
src="test.jpg"
alt="Test image"
width="100px"
height="150px"
rounded
circle
/>
);
const wrapper = screen.getByTestId(IMAGE_WRAPPER_TEST_ID); // see below for adding this
expect(wrapper).toHaveStyle({
width: "100px",
height: "150px",
"border-radius": "50%", // circle true means border-radius 50%
});
});
it("applies border-radius 12px when rounded is true and circle is false", () => {
const { getByTestId } = render(
<Image rounded={true} circle={false} src="test.jpg" alt="Test image" />
);
const wrapper = getByTestId(IMAGE_WRAPPER_TEST_ID);
expect(wrapper).toHaveStyle("border-radius: 12px");
});

it("applies border-radius 0 when both circle and rounded are false", () => {
const { getByTestId } = render(
<Image rounded={false} circle={false} src="test.jpg" alt="Test image" />
);
const wrapper = getByTestId(IMAGE_WRAPPER_TEST_ID);
expect(wrapper).toHaveStyle("border-radius: 0");
});

it("sets object-fit to 'cover' when cover is true", () => {
render(<Image src="test.jpg" alt="Test image" cover={true} />);
const image = screen.getByAltText("Test image");
expect(image).toHaveStyle("object-fit: cover");
});

});
5 changes: 3 additions & 2 deletions frontend/src/components/atoms/Image/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { ReactNode } from "react";
import { ImageWrapper, StyledImage, Overlay, OverlayContent } from "./index.styles";
import { IMAGE_WRAPPER_TEST_ID, OVERLAY_TEST_ID } from "../../../utils/constants";

interface IImage extends React.ImgHTMLAttributes<HTMLImageElement> {
width?: string;
Expand All @@ -22,9 +23,9 @@ const Image: React.FC<IImage> = ({
...props
}) => {
return (
<ImageWrapper width={width} height={height} rounded={rounded} circle={circle}>
<ImageWrapper data-testid={IMAGE_WRAPPER_TEST_ID} width={width} height={height} rounded={rounded} circle={circle}>
<StyledImage {...props} cover={cover} />
{overlay && <Overlay />}
{overlay && <Overlay data-testid={OVERLAY_TEST_ID} />}
{overlayContent && <OverlayContent>{overlayContent}</OverlayContent>}
</ImageWrapper>
);
Expand Down
51 changes: 51 additions & 0 deletions frontend/src/components/atoms/Loader/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import OAuthLoader from "./index";
import { ThemeProvider } from "styled-components";
import {theme } from "../../../styles/theme";
import { LOADER_TEST_ID } from "../../../utils/constants";

const renderWithTheme = (iconUIComp: React.ReactNode) => {
return render(<ThemeProvider theme={theme}>{iconUIComp}</ThemeProvider>);
};

describe("OAuthLoader Component", () => {
it("should renders loading message by default", () => {
renderWithTheme(<OAuthLoader />);
expect(
screen.getByText("Authenticating with Google...")
).toBeInTheDocument();
});

it("should renders redirecting message when loading is false", () => {
renderWithTheme(<OAuthLoader loading={false} />);
expect(screen.getByText("Redirecting...")).toBeInTheDocument();
});

it("should applies theme-based styles correctly", () => {
renderWithTheme(<OAuthLoader />);
const wrapper = screen.getByText("Authenticating with Google...").parentElement;
const message = screen.getByText("Authenticating with Google...")
expect(message).toHaveStyle(
`font-size: ${theme.fontSizes.lg}`
)
expect(wrapper).toHaveStyle(
`background-color: ${theme.colors.background}`
);
expect(wrapper).toHaveStyle(
`color: ${theme.colors.textPrimary}`
);
expect(wrapper).toHaveStyle(
`padding: ${theme.spacing.lg}`
);
});

it("should renders spinner with correct border color", () => {
renderWithTheme(<OAuthLoader />);
const spinner = screen.getByTestId(LOADER_TEST_ID);
expect(spinner).toHaveStyle(
`border: 4px solid ${theme.colors.primary}`
);
});
});
3 changes: 2 additions & 1 deletion frontend/src/components/atoms/Loader/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { Wrapper, Spinner, Message, SubText } from "./index.styles";
import { LOADER_TEST_ID } from "../../../utils/constants";

interface ILoader {
loading?: boolean;
Expand All @@ -8,7 +9,7 @@ interface ILoader {
const OAuthLoader: React.FC<ILoader> = ({ loading = true }) => {
return (
<Wrapper>
<Spinner />
<Spinner data-testid={LOADER_TEST_ID} />
<Message>{loading ? "Authenticating with Google..." : "Redirecting..."}</Message>
<SubText>Please wait while we set up your account and route you.</SubText>
</Wrapper>
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/components/atoms/Logo/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Logo } from "./index";
import { theme } from "../../../styles/theme";
import { ThemeProvider } from "styled-components";
import { SCISSOR_ICON_TEST_ID } from "../../../utils/constants";

const renderWithTheme = (iconUIComp: React.ReactNode) => {
return render(<ThemeProvider theme={theme}>{iconUIComp}</ThemeProvider>);
};

describe("Logo component", () => {
it("should render the scissor icon", () => {
renderWithTheme(<Logo />);
const svg = screen.getByTestId(SCISSOR_ICON_TEST_ID);
expect(svg).toBeInTheDocument();
});

it("should render the text 'Barber'", () => {
renderWithTheme(<Logo />);
expect(screen.getByText("Barber")).toBeInTheDocument();
});

it("should render the Typography with text 'PRO'", () => {
renderWithTheme(<Logo />);
expect(screen.getByText("PRO")).toBeInTheDocument();
});

it("should render the Typography with correct accent color", () => {
renderWithTheme(<Logo />);
const proElement = screen.getByText("PRO");
expect(proElement).toHaveStyle(`color: ${theme.colors.accent}`);
});

it("should apply correct styles to the svg icon", () => {
renderWithTheme(<Logo />);
const svg = screen.getByTestId(SCISSOR_ICON_TEST_ID);
expect(svg).toHaveStyle(`font-size: 2rem`);
expect(svg).toHaveStyle(`color: ${theme.colors.accent}`);
});
});
3 changes: 2 additions & 1 deletion frontend/src/components/atoms/Logo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { FaCut } from "react-icons/fa"; // Scissor Icon
import { theme } from "../../../styles/theme";
import Typography from "../Typography";
import { LogoWrapper } from "./index.styles";
import { SCISSOR_ICON_TEST_ID } from "../../../utils/constants";

export const Logo = () => {
return (
<LogoWrapper>
<FaCut />
<FaCut data-testid={SCISSOR_ICON_TEST_ID} />
Barber
<Typography
as="h1"
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/components/atoms/ScissorsBackground/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";

import ScissorsBackground from "./index";

import { SCISSORS_COUNT, SCISSORS_WRAPPER_TEST_ID } from "../../../utils/constants";
describe("ScissorsBackground component", () => {
it("should render the ScissorsWrapper container", () => {
render(<ScissorsBackground />);
const wrapper = screen.getByTestId(SCISSORS_WRAPPER_TEST_ID);
expect(wrapper).toBeInTheDocument();
});

it(`should render exactly ${SCISSORS_COUNT} scissors`, () => {
render(<ScissorsBackground />);
const scissors = screen.getAllByTestId(/scissor-/);
expect(scissors.length).toBe(SCISSORS_COUNT);
});

it("should render divs with correct test ids", () => {
render(<ScissorsBackground />);
for (let i = 1; i <= SCISSORS_COUNT; i++) {
expect(screen.getByTestId(`scissor-${i}`)).toBeInTheDocument();
}
});
});
Loading
Loading