This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
app/apps/api/src/__tests__/slots.test.ts
T
groombook-paperclip[bot] cba502e35f Set up unit testing infrastructure
Extract slot generation from book.ts into pure utility for unit testing.
Add 8 API unit tests and 4 web component tests with coverage thresholds.

Closes #39

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 01:55:02 +00:00

117 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from "vitest";
import {
generateAvailableSlots,
BUSINESS_START_HOUR,
BUSINESS_END_HOUR,
} from "../lib/slots.js";
const DATE = "2026-03-18";
const G1 = "groomer-1";
const G2 = "groomer-2";
function utc(h: number, m = 0): Date {
const d = new Date(`${DATE}T00:00:00Z`);
d.setUTCHours(h, m, 0, 0);
return d;
}
describe("generateAvailableSlots", () => {
it("returns slots within business hours", () => {
const slots = generateAvailableSlots({
dateStr: DATE,
durationMinutes: 60,
groomerIds: [G1],
booked: [],
});
expect(slots.length).toBeGreaterThan(0);
slots.forEach((s) => {
const h = new Date(s).getUTCHours();
expect(h).toBeGreaterThanOrEqual(BUSINESS_START_HOUR);
expect(h).toBeLessThan(BUSINESS_END_HOUR);
});
});
it("returns correct count of 60-min slots across 8-hour window", () => {
// 09:0017:00 = 8 hours → 8 one-hour slots
const slots = generateAvailableSlots({
dateStr: DATE,
durationMinutes: 60,
groomerIds: [G1],
booked: [],
});
expect(slots).toHaveLength(8);
});
it("returns empty array when no groomers", () => {
const slots = generateAvailableSlots({
dateStr: DATE,
durationMinutes: 60,
groomerIds: [],
booked: [],
});
expect(slots).toHaveLength(0);
});
it("excludes slots blocked by a booking", () => {
const slots = generateAvailableSlots({
dateStr: DATE,
durationMinutes: 60,
groomerIds: [G1],
booked: [{ staffId: G1, startTime: utc(9), endTime: utc(10) }],
});
expect(slots).not.toContain(new Date(`${DATE}T09:00:00.000Z`).toISOString());
expect(slots).toContain(new Date(`${DATE}T10:00:00.000Z`).toISOString());
});
it("keeps slot available when only the other groomer is booked", () => {
const slots = generateAvailableSlots({
dateStr: DATE,
durationMinutes: 60,
groomerIds: [G1, G2],
booked: [{ staffId: G1, startTime: utc(9), endTime: utc(10) }],
});
// G2 is free at 09:00 so slot should still appear
expect(slots).toContain(new Date(`${DATE}T09:00:00.000Z`).toISOString());
});
it("excludes a slot only when ALL groomers are booked", () => {
const slots = generateAvailableSlots({
dateStr: DATE,
durationMinutes: 60,
groomerIds: [G1, G2],
booked: [
{ staffId: G1, startTime: utc(9), endTime: utc(10) },
{ staffId: G2, startTime: utc(9), endTime: utc(10) },
],
});
expect(slots).not.toContain(new Date(`${DATE}T09:00:00.000Z`).toISOString());
});
it("correctly handles a booking that partially overlaps a slot", () => {
// Booking 09:3010:30 should block the 09:00 and 10:00 slots for G1
const slots = generateAvailableSlots({
dateStr: DATE,
durationMinutes: 60,
groomerIds: [G1],
booked: [{ staffId: G1, startTime: utc(9, 30), endTime: utc(10, 30) }],
});
expect(slots).not.toContain(new Date(`${DATE}T09:00:00.000Z`).toISOString());
expect(slots).not.toContain(new Date(`${DATE}T10:00:00.000Z`).toISOString());
expect(slots).toContain(new Date(`${DATE}T11:00:00.000Z`).toISOString());
});
it("does not generate a slot that would exceed business hours end", () => {
// 30-min slots: last valid start is 16:30 (ends at 17:00)
const slots = generateAvailableSlots({
dateStr: DATE,
durationMinutes: 30,
groomerIds: [G1],
booked: [],
});
const last = slots[slots.length - 1];
expect(last).toBeDefined();
expect(new Date(last!).getUTCHours()).toBe(16);
expect(new Date(last!).getUTCMinutes()).toBe(30);
});
});