feat: added initial implementation

This commit is contained in:
2026-03-14 23:30:56 +03:00
parent 8b626a940e
commit 2a8b6a7b62
19 changed files with 964 additions and 79 deletions
+100
View File
@@ -0,0 +1,100 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { vi } from "vitest";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import BookPage from "./BookPage";
import { AuthProvider } from "../contexts/AuthContext";
import i18n from "../i18n";
vi.mock("../components/ProtectedRoute", () => ({
default: ({ children }) => <>{children}</>,
}));
vi.mock("../api/client", () => ({
apiGet: vi.fn(),
apiPost: vi.fn(),
}));
const { apiGet, apiPost } = await import("../api/client");
function renderBook(initialEntries = ["/book?salon=1"]) {
return render(
<AuthProvider>
<MemoryRouter initialEntries={initialEntries}>
<Routes>
<Route path="/book" element={<BookPage />} />
<Route path="/pay" element={<div>Pay Page</div>} />
</Routes>
</MemoryRouter>
</AuthProvider>
);
}
const salonFixture = {
id: 1,
name: "Riyadh Salon",
services: [
{ id: 10, name: "Cut", duration_minutes: 60, price_amount: 120, currency: "SAR" },
],
staff: [{ id: 99, name: "Mona" }],
};
describe("BookPage", () => {
beforeEach(async () => {
vi.clearAllMocks();
apiGet.mockResolvedValue(salonFixture);
await i18n.changeLanguage("en");
});
it("validates required fields", async () => {
renderBook();
await screen.findByText("Riyadh Salon");
fireEvent.click(screen.getByRole("button", { name: "Confirm booking" }));
expect(screen.getByText("Please fill all required fields.")).toBeInTheDocument();
});
it("submits booking and redirects to payment", async () => {
apiPost.mockResolvedValueOnce({ id: 55 });
renderBook();
await screen.findByText("Riyadh Salon");
fireEvent.change(screen.getByLabelText("Service"), { target: { value: "10" } });
fireEvent.change(screen.getByLabelText("Staff"), { target: { value: "99" } });
fireEvent.change(screen.getByLabelText("Date"), { target: { value: "2026-03-14" } });
fireEvent.change(screen.getByLabelText("Time"), { target: { value: "10:30" } });
fireEvent.click(screen.getByRole("button", { name: "Confirm booking" }));
await waitFor(() => {
expect(screen.getByText("Pay Page")).toBeInTheDocument();
});
expect(apiPost).toHaveBeenCalledWith(
"/bookings/",
expect.objectContaining({
service: 10,
staff: 99,
start_time: "2026-03-14T10:30:00+03:00",
end_time: expect.any(String),
}),
null
);
const payload = apiPost.mock.calls[0][1];
const startMs = new Date(payload.start_time).getTime();
const endMs = new Date(payload.end_time).getTime();
expect(endMs - startMs).toBe(60 * 60 * 1000);
});
it("shows API error message", async () => {
apiPost.mockRejectedValueOnce(new Error("Booking failed"));
renderBook();
await screen.findByText("Riyadh Salon");
fireEvent.change(screen.getByLabelText("Service"), { target: { value: "10" } });
fireEvent.change(screen.getByLabelText("Staff"), { target: { value: "99" } });
fireEvent.change(screen.getByLabelText("Date"), { target: { value: "2026-03-14" } });
fireEvent.change(screen.getByLabelText("Time"), { target: { value: "10:30" } });
fireEvent.click(screen.getByRole("button", { name: "Confirm booking" }));
await waitFor(() => {
expect(screen.getByText("Booking failed")).toBeInTheDocument();
});
});
});