Set up unit testing infrastructure

- Extract slot generation logic into apps/api/src/lib/slots.ts for testability
- Add 8 unit tests covering slot generation edge cases (overlap, multi-groomer, boundary)
- Add @testing-library/react + jsdom to apps/web; configure vitest with jsdom environment
- Add 4 component tests for App navigation rendering and active-link highlighting
- Remove passWithNoTests: true from both vitest configs; add coverage thresholds

Closes #39

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Groom Book CTO
2026-03-18 01:52:36 +00:00
parent 817a76f8d5
commit 71ec6f1d70
10 changed files with 1046 additions and 26 deletions
+5
View File
@@ -18,10 +18,15 @@
"react-router-dom": "^7.1.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.0.6",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^3.2.4",
"eslint": "^9.18.0",
"jsdom": "^26.1.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0",
"vite": "^6.0.7",
+58
View File
@@ -0,0 +1,58 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, within, act } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { App } from "../App.js";
// Prevent fetch errors from page components loading data on mount
beforeEach(() => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => [],
} as unknown as Response);
});
async function renderApp(route = "/") {
await act(async () => {
render(
<MemoryRouter initialEntries={[route]}>
<App />
</MemoryRouter>
);
});
return screen.getByRole("navigation");
}
describe("App navigation", () => {
it("renders the Groom Book brand", async () => {
const nav = await renderApp();
expect(within(nav).getByText("Groom Book")).toBeInTheDocument();
});
it("renders the Book CTA button", async () => {
const nav = await renderApp();
expect(within(nav).getByText("Book")).toBeInTheDocument();
});
it("renders all primary nav links", async () => {
const nav = await renderApp();
const expectedLinks = [
"Appointments",
"Clients",
"Services",
"Staff",
"Invoices",
"Group Bookings",
"Reports",
];
expectedLinks.forEach((label) => {
expect(within(nav).getByText(label)).toBeInTheDocument();
});
});
it("highlights the active route link", async () => {
const nav = await renderApp("/clients");
const clientsLink = within(nav).getByText("Clients");
// Active links use fontWeight 600
expect(clientsLink).toHaveStyle({ fontWeight: "600" });
});
});
+1
View File
@@ -0,0 +1 @@
import "@testing-library/jest-dom";
+14 -1
View File
@@ -1,7 +1,20 @@
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
passWithNoTests: true,
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
globals: true,
coverage: {
provider: "v8",
include: ["src/**"],
exclude: ["src/test/**", "src/main.tsx"],
thresholds: {
lines: 50,
functions: 50,
},
},
},
});