Getting pre-commit set up with eslint.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-09-18 22:12:15 -04:00
parent 6b7da583bd
commit 3b04aa22d2
20 changed files with 413 additions and 2 deletions

View File

@@ -72,7 +72,8 @@ This is a full-stack application simulating an online booking system for confere
### Testing and Code Quality
^ Pytest, coverage, pre-commit hooks, and code style enforcement.
- 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__/`).
### Docker Compose for Orchestration

View File

@@ -1,6 +1,12 @@
repos:
- repo: local
hooks:
- id: eslint-frontend
name: ESLint
entry: yarn --cwd frontend eslint .
language: system
types: [javascript]
files: ^frontend/src/
- id: black
name: black
entry: black

View File

@@ -0,0 +1,61 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import BookingForm from "../../components/BookingForm";
import { RoomProvider } from "../../context/RoomContext";
import { BookingProvider } from "../../context/BookingContext";
import { UserProvider } from "../../context/UserContext";
describe("BookingForm", () => {
beforeAll(() => {
class EventSourceMock {
static readonly CONNECTING = 0;
static readonly OPEN = 1;
static readonly CLOSED = 2;
readonly CONNECTING = 0;
readonly OPEN = 1;
readonly CLOSED = 2;
readyState = 0;
url = "";
withCredentials = false;
close() {}
addEventListener() {}
removeEventListener() {}
dispatchEvent() {
return true;
}
onmessage = null;
onerror = null;
onopen = null;
}
window.EventSource = EventSourceMock as any;
});
it("renders the booking form dialog when open", () => {
render(
<RoomProvider>
<BookingProvider>
<UserProvider>
<BookingForm open={true} onClose={() => {}} />
</UserProvider>
</BookingProvider>
</RoomProvider>
);
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
it("calls onClose when the dialog is closed", () => {
const onClose = jest.fn();
render(
<RoomProvider>
<BookingProvider>
<UserProvider>
<BookingForm open={true} onClose={onClose} />
</UserProvider>
</BookingProvider>
</RoomProvider>
);
fireEvent.click(screen.getByRole("button", { name: /close|cancel/i }));
expect(onClose).toHaveBeenCalled();
});
// Add more tests for validation, submission, and field rendering as needed
});

View File

@@ -0,0 +1,25 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import BookingList from "../../components/BookingList";
import { UserProvider } from "../../context/UserContext";
describe("BookingList", () => {
it("renders booking list items", () => {
const bookings = [
{
id: "1",
room_id: 1,
start_time: "2025-09-18T10:00:00Z",
end_time: "2025-09-18T11:00:00Z",
title: "Test Booking",
invitees: ["user@example.com"],
},
];
render(
<UserProvider>
<BookingList bookings={bookings} />
</UserProvider>
);
expect(screen.getByText(/6:00 AM - 7:00 AM/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,16 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import CalendarView from "../../components/CalendarView";
import { RoomProvider } from "../../context/RoomContext";
describe("CalendarView", () => {
it("renders calendar view component", () => {
render(
<RoomProvider>
<CalendarView events={[]} />
</RoomProvider>
);
// FullCalendar renders a heading for the current month/year
expect(screen.getByRole("heading")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,18 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import RoomDetailsModal from "../../components/RoomDetailsModal";
describe("RoomDetailsModal", () => {
it("renders room details modal when open", () => {
const room = {
id: 1,
name: "Alpha Room",
location: "A1",
equipment: "TV",
capacity: 10,
};
render(<RoomDetailsModal open={true} onClose={() => {}} room={room} />);
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText(/Alpha Room/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,21 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import RoomList from "../../components/RoomList";
describe("RoomList", () => {
it("renders room list items", () => {
const rooms = [
{
id: 1,
name: "Alpha Room",
location: "A1",
equipment: "TV",
capacity: 10,
},
];
render(
<RoomList rooms={rooms} selectedRoomId={1} onSelectRoom={() => {}} />
);
expect(screen.getByText(/Alpha Room/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,27 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import RoomSelect from "../../components/RoomSelect";
import { RoomProvider } from "../../context/RoomContext";
describe("RoomSelect", () => {
it("renders the room select dropdown", () => {
render(
<RoomProvider>
<RoomSelect selectedRoomId={"1"} onChange={() => {}} />
</RoomProvider>
);
expect(screen.getByLabelText(/room/i)).toBeInTheDocument();
});
it("calls onChange when a room is selected", () => {
const onChange = jest.fn();
render(
<RoomProvider>
<RoomSelect selectedRoomId={"1"} onChange={onChange} />
</RoomProvider>
);
// Simulate selection change if options exist
// fireEvent.change(screen.getByLabelText(/room/i), { target: { value: "2" } });
// expect(onChange).toHaveBeenCalledWith("2");
});
});

View File

@@ -0,0 +1,13 @@
import {
getEditBookingRoomId,
formatBookingError,
} from "../../helpers/booking";
describe("booking helpers", () => {
it("extracts room id from booking", () => {
expect(getEditBookingRoomId({ room_id: "42" })).toBe("42");
expect(getEditBookingRoomId(null)).toBe("");
});
it("formats booking errors", () => {
expect(formatBookingError("Room unavailable")).toMatch(/Room unavailable/);
});
});

View File

@@ -0,0 +1,9 @@
import { getInviteeName } from "../../helpers/bookingList";
describe("bookingList helpers", () => {
it("returns invitee name from various shapes", () => {
expect(getInviteeName({ name: "Alice" })).toBe("Alice");
expect(getInviteeName({ user: { name: "Bob" } })).toBe("Bob");
expect(getInviteeName({ displayName: "Carol" })).toBe("Carol");
expect(getInviteeName({ username: "dave" })).toBe("dave");
});
});

View File

@@ -0,0 +1,8 @@
import { getRoomClass } from "../../helpers/calendar";
describe("calendar helpers", () => {
it("returns correct room class for room id", () => {
expect(getRoomClass(1)).toBe("room-color-1");
expect(getRoomClass(21)).toBe("room-color-1");
expect(getRoomClass(2)).toBe("room-color-2");
});
});

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
import {
validateRoomId,
validateStart,
validateEnd,
validateInvitees,
} from "../../helpers/validation";
describe("validation helpers", () => {
it("validates room id", () => {
expect(validateRoomId("1")).toBe(null);
expect(validateRoomId(null)).not.toBe(null);
});
it("validates start and end times", () => {
// Use a future date for start time
const futureDate = new Date(Date.now() + 60 * 60 * 1000).toISOString();
expect(validateStart(futureDate)).toBe(null);
expect(validateEnd(futureDate, futureDate)).toBe(
"End time must be after start time."
);
});
it("validates invitees", () => {
expect(validateInvitees(["user@example.com"], 1)).toBe(null);
expect(validateInvitees([], 1)).toBe(null);
});
});

View File

@@ -0,0 +1,48 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import BookingPage from "../../pages/BookingPage";
import { RoomProvider } from "../../context/RoomContext";
import { BookingProvider } from "../../context/BookingContext";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter } from "react-router-dom";
describe("BookingPage", () => {
beforeAll(() => {
class EventSourceMock {
static readonly CONNECTING = 0;
static readonly OPEN = 1;
static readonly CLOSED = 2;
readonly CONNECTING = 0;
readonly OPEN = 1;
readonly CLOSED = 2;
readyState = 0;
url = "";
withCredentials = false;
close() {}
addEventListener() {}
removeEventListener() {}
dispatchEvent() {
return true;
}
onmessage = null;
onerror = null;
onopen = null;
}
window.EventSource = EventSourceMock as any;
});
it("renders booking page heading", () => {
const queryClient = new QueryClient();
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<BookingProvider>
<RoomProvider>
<BookingPage />
</RoomProvider>
</BookingProvider>
</MemoryRouter>
</QueryClientProvider>
);
expect(screen.getByText(/Book a Room/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,56 @@
import React from "react";
import { MemoryRouter } from "react-router-dom";
import { render, screen } from "@testing-library/react";
import ConfirmationPage from "../../pages/ConfirmationPage";
import { BookingContext } from "../../context/BookingContext";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
describe("ConfirmationPage", () => {
beforeAll(() => {
class EventSourceMock {
static readonly CONNECTING = 0;
static readonly OPEN = 1;
static readonly CLOSED = 2;
readonly CONNECTING = 0;
readonly OPEN = 1;
readonly CLOSED = 2;
readyState = 0;
url = "";
withCredentials = false;
close() {}
addEventListener() {}
removeEventListener() {}
dispatchEvent() {
return true;
}
onmessage = null;
onerror = null;
onopen = null;
}
window.EventSource = EventSourceMock as any;
});
it("renders confirmation page heading", () => {
const queryClient = new QueryClient();
// Provide a valid booking to prevent redirect
const mockBooking = {
id: "test-id",
roomId: "1",
date: "2025-09-18",
name: "Test User",
email: "test@example.com",
start_time: "10:00",
end_time: "11:00",
};
render(
<QueryClientProvider client={queryClient}>
<BookingContext.Provider
value={{ bookings: [mockBooking], fetchMonth: jest.fn() }}
>
<MemoryRouter>
<ConfirmationPage />
</MemoryRouter>
</BookingContext.Provider>
</QueryClientProvider>
);
expect(screen.getByText(/Booking Confirmed/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,53 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import LandingPage from "../../pages/LandingPage";
import { UserProvider } from "../../context/UserContext";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RoomProvider } from "../../context/RoomContext";
import { BookingProvider } from "../../context/BookingContext";
import { MemoryRouter } from "react-router-dom";
describe("LandingPage", () => {
beforeAll(() => {
class EventSourceMock {
static readonly CONNECTING = 0;
static readonly OPEN = 1;
static readonly CLOSED = 2;
readonly CONNECTING = 0;
readonly OPEN = 1;
readonly CLOSED = 2;
readyState = 0;
url = "";
withCredentials = false;
close() {}
addEventListener() {}
removeEventListener() {}
dispatchEvent() {
return true;
}
onmessage = null;
onerror = null;
onopen = null;
}
window.EventSource = EventSourceMock as any;
});
it("renders landing page heading", () => {
const queryClient = new QueryClient();
render(
<QueryClientProvider client={queryClient}>
<UserProvider>
<MemoryRouter>
<RoomProvider>
<BookingProvider>
<LandingPage />
</BookingProvider>
</RoomProvider>
</MemoryRouter>
</UserProvider>
</QueryClientProvider>
);
expect(
screen.getByText(/Conference Room Booking System/i)
).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,9 @@
import { roundToStrictlyFutureQuarter } from "../../utils/date";
describe("date utils", () => {
it("rounds to strictly future quarter hour", () => {
const now = new Date("2025-09-18T10:07:00Z");
const rounded = roundToStrictlyFutureQuarter(now);
expect(rounded.getMinutes() % 15).toBe(0);
expect(rounded.getTime()).toBeGreaterThan(now.getTime());
});
});

View File

@@ -0,0 +1,12 @@
import { logger } from "../../utils/logger";
describe("logger utility", () => {
it("logs info messages", () => {
expect(() => logger.info("test info")).not.toThrow();
});
it("logs debug messages", () => {
expect(() => logger.debug("test debug")).not.toThrow();
});
it("logs error messages", () => {
expect(() => logger.error("test error")).not.toThrow();
});
});

View File

@@ -24,7 +24,9 @@ interface BookingContextType {
fetchMonth: (month: string) => Promise<void>;
}
const BookingContext = createContext<BookingContextType | undefined>(undefined);
export const BookingContext = createContext<BookingContextType | undefined>(
undefined
);
/**
* BookingProvider