Getting things done.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-09-19 15:35:49 -04:00
parent 3b04aa22d2
commit 7790f17197
19 changed files with 123 additions and 66 deletions

View File

@@ -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

View File

@@ -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;
});

View File

@@ -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>
);

View File

@@ -1 +0,0 @@
// getRoomBookingsForDate is not exported from helpers/getRoomBookingsForDate.ts

View File

@@ -1 +0,0 @@
// No exported getRoomName function in helpers/room.ts

View File

@@ -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;
});

View File

@@ -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();
});
});

View File

@@ -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;
});

View File

@@ -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();
});

View File

@@ -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);

View File

@@ -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
};
}, []);

View File

@@ -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 }) => {

View File

@@ -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>

View File

@@ -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
};
}, []);

View File

@@ -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 (

View File

@@ -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) */

View File

@@ -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.

View File

@@ -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
};
}, []);

View File

@@ -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(() => {});