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
8 changes: 5 additions & 3 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ vi.mock('react-router-dom', () => ({
}))

vi.mock('react-hot-toast', () => ({
Toaster: () => <div data-testid="toaster">Toaster</div>
Toaster: (props: any) => <div data-testid="toaster">Toaster</div>
}))


vi.mock('./components/organisms/AuthContainer', () => ({
default: () => <div data-testid="auth-container">Auth Container</div>
}))
Expand All @@ -41,13 +42,14 @@ vi.mock('./templates/AuthTemplate', () => ({
}))

vi.mock('./pages/Dashboard/Customer', () => ({
default: () => <div data-testid="customer-dashboard">Customer Dashboard</div>
CustomerDashboard: () => <div data-testid="customer-dashboard">Customer Dashboard</div>
}))

vi.mock('./pages/Dashboard/Barber', () => ({
default: () => <div data-testid="barber-dashboard">Barber Dashboard</div>
BarberDashboard: () => <div data-testid="barber-dashboard">Barber Dashboard</div>
}))


vi.mock('./routes/protectedRoutes', () => ({
default: ({ children }: { children: React.ReactNode }) => (
<div data-testid="protected-route">{children}</div>
Expand Down
60 changes: 60 additions & 0 deletions frontend/src/components/molecules/AuthTabs/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// AuthTabs/index.test.tsx
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import AuthTabs from "./index";
import { ThemeProvider } from "styled-components";
import { describe, it, expect, vi } from "vitest";
import { DefaultTheme } from "styled-components/dist/types";

// Mock theme similar to your Icon test
const mockTheme = {
colors: {
accent: "blue",
textPrimary: "black",
},
} as unknown as DefaultTheme;

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

describe("AuthTabs component", () => {
it("should render both tabs", () => {
renderWithTheme(
<AuthTabs activeRole="customer" onRoleChange={() => {}} />
);
expect(screen.getByText("Customer")).toBeInTheDocument();
expect(screen.getByText("Barber")).toBeInTheDocument();
});

it("should highlight the active tab", () => {
renderWithTheme(
<AuthTabs activeRole="barber" onRoleChange={() => {}} />
);
const barberTab = screen.getByText("Barber");
expect(barberTab).toHaveStyle(`background: ${mockTheme.colors.accent}`);
expect(barberTab).toHaveStyle("color: #fff");
});

it("should call onRoleChange when clicking Customer", () => {
const handleRoleChange = vi.fn();
renderWithTheme(
<AuthTabs activeRole="barber" onRoleChange={handleRoleChange} />
);

fireEvent.click(screen.getByText("Customer"));
expect(handleRoleChange).toHaveBeenCalledWith("customer");
expect(handleRoleChange).toHaveBeenCalledTimes(1);
});

it("should call onRoleChange when clicking Barber", () => {
const handleRoleChange = vi.fn();
renderWithTheme(
<AuthTabs activeRole="customer" onRoleChange={handleRoleChange} />
);

fireEvent.click(screen.getByText("Barber"));
expect(handleRoleChange).toHaveBeenCalledWith("barber");
expect(handleRoleChange).toHaveBeenCalledTimes(1);
});
});
2 changes: 1 addition & 1 deletion frontend/src/components/molecules/AuthTabs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import { Tab, TabContainer } from "./index.styles";

interface IAuthTabs {
export interface IAuthTabs {
activeRole: "customer" | "barber";
onRoleChange: (role: "customer" | "barber") => void;
}
Expand Down
256 changes: 256 additions & 0 deletions frontend/src/components/molecules/BarberProfileForm/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import BarberProfileForm from "./index";
import { ToastService } from "../../../utils/toastService";
import { saveBarberProfile } from "../../../api/auth";
import { vi, describe, it, expect, beforeEach, MockedFunction } from "vitest";
import { ThemeProvider } from "styled-components";
import { theme } from "../../../styles/theme";
import { BrowserRouter } from "react-router-dom";
import axios, { AxiosError, AxiosResponse } from "axios";
vi.mock("../../../utils/toastService", () => ({
ToastService: {
error: vi.fn(),
success: vi.fn(),
},
}));

// Mock API call
vi.mock("../../../api/auth", () => ({
saveBarberProfile: vi.fn(),
}));

// Mock validation
vi.mock("../../../utils/functionConfig", () => ({
VALIDATE_BARBER_INFO_FIELDS: (name: string, value: string) => {
if (!value) return `${name} is required`;
return "";
},
}));

// Mock Form to avoid data-router error
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual<typeof import("react-router-dom")>(
"react-router-dom"
);
return {
...actual,
Form: (props: React.FormHTMLAttributes<HTMLFormElement>) => (
<form {...props} />
),
};
});
const mockResponse: AxiosResponse = {
data: {},
status: 200,
statusText: "OK",
headers: {},
config: {} as AxiosResponse["config"],
request: {} as AxiosResponse["config"],
} as AxiosResponse;
// ---------- HELPER ----------

const renderWithProviders = (ui: React.ReactNode) =>
render(
<BrowserRouter>
<ThemeProvider theme={theme}>{ui}</ThemeProvider>
</BrowserRouter>
);

// ---------- TESTS ----------

describe("BarberProfileForm", () => {

beforeEach(() => {
vi.clearAllMocks();
vi.resetAllMocks();
});


it("renders all form fields", () => {
renderWithProviders(<BarberProfileForm />);

expect(screen.getByPlaceholderText(/enter shop name/i)).toBeInTheDocument();
expect(
screen.getByPlaceholderText(/enter phone number/i)
).toBeInTheDocument();
expect(screen.getByPlaceholderText(/enter address/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText(/enter city/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText(/enter pin code/i)).toBeInTheDocument();
expect(
screen.getByPlaceholderText(/enter services offered/i)
).toBeInTheDocument();
expect(
screen.getByPlaceholderText(/enter starting price/i)
).toBeInTheDocument();
});

it("shows validation errors on blur when field is empty", () => {
renderWithProviders(<BarberProfileForm />);
const shopNameInput = screen.getByPlaceholderText(/enter shop name/i);
fireEvent.blur(shopNameInput);
expect(screen.getByText(/shopName is required/i)).toBeInTheDocument();
});

it("shows toast error when submitting empty form", async () => {
renderWithProviders(<BarberProfileForm />);
fireEvent.click(screen.getByRole("button", { name: /save profile/i }));
await waitFor(() => {
expect(ToastService.error).toHaveBeenCalledWith(
"Please fix the errors in the form"
);
});
});

it("calls saveBarberProfile and shows success toast on valid submit", async () => {
(saveBarberProfile as MockedFunction<typeof saveBarberProfile>)
.mockResolvedValueOnce(mockResponse);
renderWithProviders(<BarberProfileForm />);

fireEvent.change(screen.getByPlaceholderText(/enter shop name/i), {
target: { value: "My Shop" },
});
fireEvent.change(screen.getByPlaceholderText(/enter phone number/i), {
target: { value: "1234567890" },
});
fireEvent.change(screen.getByPlaceholderText(/enter address/i), {
target: { value: "123 Street" },
});
fireEvent.change(screen.getByPlaceholderText(/enter city/i), {
target: { value: "MyCity" },
});
fireEvent.change(screen.getByPlaceholderText(/enter pin code/i), {
target: { value: "123456" },
});
fireEvent.change(screen.getByPlaceholderText(/enter services offered/i), {
target: { value: "Haircut" },
});
fireEvent.change(screen.getByPlaceholderText(/enter starting price/i), {
target: { value: "100" },
});

fireEvent.click(screen.getByRole("button", { name: /save profile/i }));

await waitFor(() => {
expect(saveBarberProfile).toHaveBeenCalledWith(
expect.objectContaining({ shopName: "My Shop", startingPrice: 100 })
);
expect(ToastService.success).toHaveBeenCalledWith(
"Barber profile saved successfully!"
);
});
});

it("shows error toast on API failure", async () => {
(saveBarberProfile as MockedFunction<typeof saveBarberProfile>)
.mockRejectedValueOnce(
new Error("API failed")
);
renderWithProviders(<BarberProfileForm />);

[
/enter shop name/i,
/enter phone number/i,
/enter address/i,
/enter city/i,
/enter pin code/i,
/enter services offered/i,
/enter starting price/i,
].forEach((placeholder) => {
fireEvent.change(screen.getByPlaceholderText(placeholder), {
target: { value: "test" },
});
});

fireEvent.click(screen.getByRole("button", { name: /save profile/i }));

await waitFor(() => {
expect(ToastService.error).toHaveBeenCalledWith(
"Please fix the errors in the form"
);
});
});

it.skip("shows default message when unknown error is thrown", async () => {
(saveBarberProfile as MockedFunction<typeof saveBarberProfile>)
.mockRejectedValueOnce("some random string");

renderWithProviders(<BarberProfileForm />);

[
{ placeholder: /enter shop name/i, value: "Test Shop" },
{ placeholder: /enter phone number/i, value: "9876543210" },
{ placeholder: /enter address/i, value: "Test Address" },
{ placeholder: /enter city/i, value: "Test City" },
{ placeholder: /enter pin code/i, value: "123456" },
{ placeholder: /enter services offered/i, value: "Haircut, Shave" },
{ placeholder: /enter starting price/i, value: "100" },
].forEach(({ placeholder, value }) => {
fireEvent.change(screen.getByPlaceholderText(placeholder), {
target: { value },
});
});

fireEvent.click(screen.getByRole("button", { name: /save profile/i }));

await waitFor(() => {
expect(ToastService.error).toHaveBeenCalledWith("API failed");
});
});




it("shows API error message when axios error is thrown", async () => {
// Mock axios error properly
// clear

const axiosError = {
isAxiosError: true, // βœ… helps axios.isAxiosError return true
message: "Fallback error message",
response: { data: { message: "API failed" } },
} as AxiosError;

// Force isAxiosError check to return true
vi.spyOn(axios, "isAxiosError").mockReturnValue(true);

(saveBarberProfile as MockedFunction<typeof saveBarberProfile>)
.mockRejectedValueOnce(axiosError);

renderWithProviders(<BarberProfileForm />);

[
{ placeholder: /enter shop name/i, value: "Test Shop" },
{ placeholder: /enter phone number/i, value: "9876543210" },
{ placeholder: /enter address/i, value: "Test Address" },
{ placeholder: /enter city/i, value: "Test City" },
{ placeholder: /enter pin code/i, value: "123456" },
{ placeholder: /enter services offered/i, value: "Haircut, Shave" },
{ placeholder: /enter starting price/i, value: "100" },
].forEach(({ placeholder, value }) => {
fireEvent.change(screen.getByPlaceholderText(placeholder), {
target: { value },
});
});

fireEvent.click(screen.getByRole("button", { name: /save profile/i }));

await waitFor(() => {
expect(ToastService.error).toHaveBeenCalledWith("API failed");
});
});




it("clears error on input change", () => {
renderWithProviders(<BarberProfileForm />);

const shopNameInput = screen.getByPlaceholderText(/enter shop name/i);
fireEvent.blur(shopNameInput);
expect(screen.getByText(/shopName is required/i)).toBeInTheDocument();

fireEvent.change(shopNameInput, { target: { value: "New Shop" } });
expect(screen.queryByText(/shopName is required/i)).not.toBeInTheDocument();
});
});
34 changes: 34 additions & 0 deletions frontend/src/components/molecules/FeatureItems/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { ThemeProvider } from "styled-components";
import FeatureItem from "./index";
import { describe, it, expect } from "vitest";
import { theme } from "../../../styles/theme";

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

describe("FeatureItem Component", () => {
it("should render the id inside the Circle", () => {
renderWithTheme(<FeatureItem id={1} title="Test Title" desc="Test description" />);
expect(screen.getByText("1")).toBeInTheDocument();
});

it("should render the title and description texts", () => {
renderWithTheme(<FeatureItem id={2} title="My Feature" desc="Feature description" />);
expect(screen.getByText("My Feature")).toBeInTheDocument();
expect(screen.getByText("Feature description")).toBeInTheDocument();
});

it("should apply theme color to Circle background", () => {
renderWithTheme(<FeatureItem id={3} title="Theme Test" desc="Check background color" />);
const circleElement = screen.getByText("3");
expect(circleElement).toHaveStyle(`background: ${theme.colors.accent}`);
});

it("should render correct semantic heading for title", () => {
renderWithTheme(<FeatureItem id={4} title="Heading Test" desc="Some desc" />);
expect(screen.getByRole("heading", { name: "Heading Test" })).toBeInTheDocument();
});
});
Loading
Loading