mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-05 23:58:25 -04:00
@@ -72,8 +72,8 @@ This is a full-stack application simulating an online booking system for confere
|
||||
|
||||
### Testing and Code Quality
|
||||
|
||||
- Pytest, coverage, pre-commit hooks (including prettier), and code style enforcement for backend
|
||||
- Jest and React Testing Library for comprehensive frontend tests (run with `yarn run jest` and maintained in `frontend/src/__tests__/`).
|
||||
- Pytest, coverage, pre-commit hooks, and code style enforcement for backend
|
||||
- Jest and React Testing Library, code style enforcement and additional pre-commit hooks for frontend
|
||||
|
||||
### Docker Compose for Orchestration
|
||||
|
||||
|
||||
@@ -7,7 +7,15 @@ import { UserProvider } from "../../context/UserContext";
|
||||
|
||||
describe("BookingForm", () => {
|
||||
beforeAll(() => {
|
||||
class EventSourceMock {
|
||||
/**
|
||||
* Mock implementation of EventSource for tests.
|
||||
* Implements the minimal EventSource interface.
|
||||
*/
|
||||
/**
|
||||
* Mock implementation of EventSource for tests.
|
||||
* TypeScript: 'as any' is used to bypass constructor signature requirements.
|
||||
*/
|
||||
class EventSourceMock implements EventSource {
|
||||
static readonly CONNECTING = 0;
|
||||
static readonly OPEN = 1;
|
||||
static readonly CLOSED = 2;
|
||||
@@ -23,9 +31,9 @@ describe("BookingForm", () => {
|
||||
dispatchEvent() {
|
||||
return true;
|
||||
}
|
||||
onmessage = null;
|
||||
onerror = null;
|
||||
onopen = null;
|
||||
onmessage: ((this: EventSource, ev: MessageEvent) => any) | null = null;
|
||||
onerror: ((this: EventSource, ev: Event) => any) | null = null;
|
||||
onopen: ((this: EventSource, ev: Event) => any) | null = null;
|
||||
}
|
||||
window.EventSource = EventSourceMock as any;
|
||||
});
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @file RoomSelect.test.tsx
|
||||
* Unit tests for the RoomSelect component.
|
||||
* Ensures dropdown renders and onChange is called.
|
||||
*/
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import RoomSelect from "../../components/RoomSelect";
|
||||
@@ -5,8 +10,11 @@ import { RoomProvider } from "../../context/RoomContext";
|
||||
|
||||
describe("RoomSelect", () => {
|
||||
it("renders the room select dropdown", () => {
|
||||
const rooms = [
|
||||
{ id: "1", name: "Test Room", capacity: 2, amenities: [], price: 100 },
|
||||
];
|
||||
render(
|
||||
<RoomProvider>
|
||||
<RoomProvider value={{ rooms, fetchRooms: jest.fn() }}>
|
||||
<RoomSelect selectedRoomId={"1"} onChange={() => {}} />
|
||||
</RoomProvider>
|
||||
);
|
||||
@@ -15,8 +23,11 @@ describe("RoomSelect", () => {
|
||||
|
||||
it("calls onChange when a room is selected", () => {
|
||||
const onChange = jest.fn();
|
||||
const rooms = [
|
||||
{ id: "1", name: "Test Room", capacity: 2, amenities: [], price: 100 },
|
||||
];
|
||||
render(
|
||||
<RoomProvider>
|
||||
<RoomProvider value={{ rooms, fetchRooms: jest.fn() }}>
|
||||
<RoomSelect selectedRoomId={"1"} onChange={onChange} />
|
||||
</RoomProvider>
|
||||
);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
// getRoomBookingsForDate is not exported from helpers/getRoomBookingsForDate.ts
|
||||
@@ -1 +0,0 @@
|
||||
// No exported getRoomName function in helpers/room.ts
|
||||
@@ -8,7 +8,15 @@ import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
describe("BookingPage", () => {
|
||||
beforeAll(() => {
|
||||
class EventSourceMock {
|
||||
/**
|
||||
* Mock implementation of EventSource for tests.
|
||||
* Implements the minimal EventSource interface.
|
||||
*/
|
||||
/**
|
||||
* Mock implementation of EventSource for tests.
|
||||
* TypeScript: 'as any' is used to bypass constructor signature requirements.
|
||||
*/
|
||||
class EventSourceMock implements EventSource {
|
||||
static readonly CONNECTING = 0;
|
||||
static readonly OPEN = 1;
|
||||
static readonly CLOSED = 2;
|
||||
@@ -24,9 +32,9 @@ describe("BookingPage", () => {
|
||||
dispatchEvent() {
|
||||
return true;
|
||||
}
|
||||
onmessage = null;
|
||||
onerror = null;
|
||||
onopen = null;
|
||||
onmessage: ((this: EventSource, ev: MessageEvent) => any) | null = null;
|
||||
onerror: ((this: EventSource, ev: Event) => any) | null = null;
|
||||
onopen: ((this: EventSource, ev: Event) => any) | null = null;
|
||||
}
|
||||
window.EventSource = EventSourceMock as any;
|
||||
});
|
||||
|
||||
@@ -2,11 +2,20 @@ import React from "react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import ConfirmationPage from "../../pages/ConfirmationPage";
|
||||
import * as roomsApi from "../../apis/rooms";
|
||||
import { BookingContext } from "../../context/BookingContext";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
describe("ConfirmationPage", () => {
|
||||
beforeAll(() => {
|
||||
class EventSourceMock {
|
||||
/**
|
||||
* Mock implementation of EventSource for tests.
|
||||
* Implements the minimal EventSource interface.
|
||||
*/
|
||||
/**
|
||||
* Mock implementation of EventSource for tests.
|
||||
* TypeScript: 'as any' is used to bypass constructor signature requirements.
|
||||
*/
|
||||
class EventSourceMock implements EventSource {
|
||||
static readonly CONNECTING = 0;
|
||||
static readonly OPEN = 1;
|
||||
static readonly CLOSED = 2;
|
||||
@@ -22,35 +31,48 @@ describe("ConfirmationPage", () => {
|
||||
dispatchEvent() {
|
||||
return true;
|
||||
}
|
||||
onmessage = null;
|
||||
onerror = null;
|
||||
onopen = null;
|
||||
onmessage: ((this: EventSource, ev: MessageEvent) => any) | null = null;
|
||||
onerror: ((this: EventSource, ev: Event) => any) | null = null;
|
||||
onopen: ((this: EventSource, ev: Event) => any) | null = null;
|
||||
}
|
||||
window.EventSource = EventSourceMock as any;
|
||||
});
|
||||
it("renders confirmation page heading", () => {
|
||||
it("renders confirmation page heading", async () => {
|
||||
// Setup required mocks and context
|
||||
jest.spyOn(roomsApi, "getRooms").mockResolvedValue([
|
||||
{
|
||||
id: "1",
|
||||
name: "Test Room",
|
||||
capacity: 2,
|
||||
amenities: [],
|
||||
price: 100,
|
||||
},
|
||||
]);
|
||||
const queryClient = new QueryClient();
|
||||
// Provide a valid booking to prevent redirect
|
||||
const mockBooking = {
|
||||
id: "test-id",
|
||||
id: "b1",
|
||||
roomId: "1",
|
||||
date: "2025-09-18",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
guestName: "Test Guest",
|
||||
date: "2025-09-19",
|
||||
start_time: "10:00",
|
||||
end_time: "11:00",
|
||||
status: "confirmed",
|
||||
};
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BookingContext.Provider
|
||||
value={{ bookings: [mockBooking], fetchMonth: jest.fn() }}
|
||||
>
|
||||
<MemoryRouter>
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
{ pathname: "/confirmation", state: { booking: mockBooking } },
|
||||
]}
|
||||
>
|
||||
<ConfirmationPage />
|
||||
</MemoryRouter>
|
||||
</BookingContext.Provider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
expect(screen.getByText(/Booking Confirmed/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/Booking Confirmed/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,15 @@ import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
describe("LandingPage", () => {
|
||||
beforeAll(() => {
|
||||
class EventSourceMock {
|
||||
/**
|
||||
* Mock implementation of EventSource for tests.
|
||||
* Implements the minimal EventSource interface.
|
||||
*/
|
||||
/**
|
||||
* Mock implementation of EventSource for tests.
|
||||
* TypeScript: 'as any' is used to bypass constructor signature requirements.
|
||||
*/
|
||||
class EventSourceMock implements EventSource {
|
||||
static readonly CONNECTING = 0;
|
||||
static readonly OPEN = 1;
|
||||
static readonly CLOSED = 2;
|
||||
@@ -25,9 +33,9 @@ describe("LandingPage", () => {
|
||||
dispatchEvent() {
|
||||
return true;
|
||||
}
|
||||
onmessage = null;
|
||||
onerror = null;
|
||||
onopen = null;
|
||||
onmessage: ((this: EventSource, ev: MessageEvent) => any) | null = null;
|
||||
onerror: ((this: EventSource, ev: Event) => any) | null = null;
|
||||
onopen: ((this: EventSource, ev: Event) => any) | null = null;
|
||||
}
|
||||
window.EventSource = EventSourceMock as any;
|
||||
});
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
/**
|
||||
* @file logger.test.ts
|
||||
* Unit tests for the logger utility.
|
||||
* Ensures logger methods do not throw and are properly typed.
|
||||
*/
|
||||
import { logger } from "../../utils/logger";
|
||||
|
||||
describe("logger utility", () => {
|
||||
beforeAll(() => {
|
||||
jest.spyOn(console, "info").mockImplementation(() => {});
|
||||
jest.spyOn(console, "debug").mockImplementation(() => {});
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
it("logs info messages", () => {
|
||||
expect(() => logger.info("test info")).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -62,9 +62,9 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
onBookingSuccess,
|
||||
}) => {
|
||||
React.useEffect(() => {
|
||||
logger.info("[BookingForm] Mounted");
|
||||
// [BookingForm] Mounted log removed
|
||||
return () => {
|
||||
logger.info("[BookingForm] Unmounted");
|
||||
// [BookingForm] Unmounted log removed
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -265,9 +265,8 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
invitees,
|
||||
};
|
||||
// Log payload for debugging
|
||||
if (window && window.console) {
|
||||
console.info("[BookingForm] updateBooking payload:", updatePayload);
|
||||
}
|
||||
// Use logger for controlled logging
|
||||
logger.info("[BookingForm] updateBooking payload:", updatePayload);
|
||||
bookingResult = await updateBooking(editBooking.id, updatePayload);
|
||||
}
|
||||
if (onBookingSuccess && bookingResult) onBookingSuccess(bookingResult);
|
||||
|
||||
@@ -32,9 +32,9 @@ const BookingList: React.FC<BookingListProps> = ({ bookings, onSelect }) => {
|
||||
const { users } = useUsers();
|
||||
|
||||
React.useEffect(() => {
|
||||
logger.info("[BookingList] Mounted");
|
||||
// [BookingList] Mounted log removed
|
||||
return () => {
|
||||
logger.info("[BookingList] Unmounted");
|
||||
// [BookingList] Unmounted log removed
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
// Utility/helper imports
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// External imports
|
||||
import React, { useEffect } from "react";
|
||||
@@ -46,14 +45,14 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
initialDate,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
logger.info("[CalendarView] Mounted");
|
||||
// [CalendarView] Mounted log removed
|
||||
return () => {
|
||||
logger.info("[CalendarView] Unmounted");
|
||||
// [CalendarView] Unmounted log removed
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
logger.debug("[CalendarView] events updated", events);
|
||||
// [CalendarView] events updated log removed
|
||||
}, [events]);
|
||||
// Custom event content with tooltip
|
||||
const renderEventContent = (arg: { event: BookingEvent }) => {
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
*/
|
||||
|
||||
// Utility/helper imports
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// External imports
|
||||
import React, { useEffect } from "react";
|
||||
import React from "react";
|
||||
|
||||
// MUI imports
|
||||
import {
|
||||
@@ -32,16 +31,7 @@ const RoomDetailsModal: React.FC<RoomDetailsModalProps> = ({
|
||||
onClose,
|
||||
room,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
logger.info("[RoomDetailsModal] Mounted");
|
||||
return () => {
|
||||
logger.info("[RoomDetailsModal] Unmounted");
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
logger.debug("[RoomDetailsModal] room updated", room);
|
||||
}, [room]);
|
||||
// Lifecycle and debug logging removed
|
||||
if (!room) return null;
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
||||
|
||||
@@ -36,9 +36,9 @@ const RoomList: FC<RoomListProps> = ({
|
||||
onSelectRoom,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
logger.info("[RoomList] Mounted");
|
||||
// [RoomList] Mounted log removed
|
||||
return () => {
|
||||
logger.info("[RoomList] Unmounted");
|
||||
// [RoomList] Unmounted log removed
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@ const RoomSelect: React.FC<RoomSelectProps> = ({
|
||||
const roomsContext = useRooms();
|
||||
const rooms = roomsContext.rooms ?? [];
|
||||
React.useEffect(() => {
|
||||
logger.info(`[RoomSelect] Mounted with ${rooms.length} rooms.`);
|
||||
// [RoomSelect] Mounted log removed
|
||||
return () => {
|
||||
logger.info("[RoomSelect] Unmounted");
|
||||
// [RoomSelect] Unmounted log removed
|
||||
};
|
||||
}, [rooms.length]);
|
||||
return (
|
||||
|
||||
@@ -62,9 +62,9 @@ const BookingPage: React.FC = () => {
|
||||
const [roomModalOpen, setRoomModalOpen] = useState(false);
|
||||
/** Track mount/unmount for analytics */
|
||||
useEffect(() => {
|
||||
logger.info("[BookingPage] Mounted");
|
||||
// [BookingPage] Mounted log removed
|
||||
return () => {
|
||||
logger.info("[BookingPage] Unmounted");
|
||||
// [BookingPage] Unmounted log removed
|
||||
};
|
||||
}, []);
|
||||
/** Get rooms from context (should be provided via context or props) */
|
||||
|
||||
@@ -28,7 +28,6 @@ import { getRooms } from "../apis/rooms";
|
||||
import { useBookings } from "../context/BookingContext";
|
||||
|
||||
// Utility/helper imports
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// Type-only imports
|
||||
import type { Booking } from "../interfaces";
|
||||
@@ -54,14 +53,14 @@ const ConfirmationPage: React.FC = () => {
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
logger.info("[ConfirmationPage] Mounted");
|
||||
// [ConfirmationPage] Mounted log removed
|
||||
return () => {
|
||||
logger.info("[ConfirmationPage] Unmounted");
|
||||
// [ConfirmationPage] Unmounted log removed
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
logger.debug("[ConfirmationPage] booking updated", booking);
|
||||
// [ConfirmationPage] booking updated log removed
|
||||
}, [booking]);
|
||||
/**
|
||||
* Subscribe to SSE for live booking status updates.
|
||||
|
||||
@@ -27,16 +27,15 @@ import { useRooms } from "../context/RoomContext";
|
||||
import { useBookings } from "../context/BookingContext";
|
||||
|
||||
// Utility/helper imports
|
||||
import { logger } from "../utils/logger";
|
||||
const LandingPage: FC = () => {
|
||||
// Log when LandingPage mounts/unmounts
|
||||
/**
|
||||
* Log when LandingPage mounts/unmounts.
|
||||
*/
|
||||
useEffect(() => {
|
||||
logger.info("[LandingPage] Mounted");
|
||||
// [LandingPage] Mounted log removed
|
||||
return () => {
|
||||
logger.info("[LandingPage] Unmounted");
|
||||
// [LandingPage] Unmounted log removed
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -10,3 +10,8 @@
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import "@testing-library/jest-dom";
|
||||
// Suppress noisy logs in test output globally before any test or import
|
||||
jest.spyOn(global.console, "info").mockImplementation(() => {});
|
||||
jest.spyOn(global.console, "warn").mockImplementation(() => {});
|
||||
jest.spyOn(global.console, "error").mockImplementation(() => {});
|
||||
jest.spyOn(global.console, "debug").mockImplementation(() => {});
|
||||
|
||||
Reference in New Issue
Block a user