Add correlation IDs and tests for monitoring
Generate short UUIDs in API route catch blocks to aid debugging. Update existing API tests to assert the correlation ID. Add unit tests for `useJourneys` and `useBikeRoute` hooks, and component tests for `EventCard` and `CalendarView`.
This commit is contained in:
+9
-15
@@ -34,26 +34,20 @@ Eliminate duplicated logic so each integration has one source of truth.
|
|||||||
|
|
||||||
| Step | ✅ Implemented | ✔️ Reviewed | Notes |
|
| Step | ✅ Implemented | ✔️ Reviewed | Notes |
|
||||||
|------|:------:|:------:|-------|
|
|------|:------:|:------:|-------|
|
||||||
| **Step 8: Add Debounce to Lookup Hooks** (~25 min) | [x] | | 400ms setTimeout + AbortController in `useGeocode.ts` and `useDestinationStation.ts`. AbortError silently ignored. |
|
| **Step 8: Add Debounce to Lookup Hooks** (~25 min) | [x] | [x] | 400ms setTimeout + AbortController in `useGeocode.ts` and `useDestinationStation.ts`. AbortError silently ignored. |
|
||||||
| **Step 9: Pre-Group Calendar Events by Date** (~20 min) | [x] | | `useMemo` builds `Map<string, Event[]>` keyed by `YYYY-MM-DD`. Per-cell `filter()` replaced with O(1) map lookup. |
|
| **Step 9: Pre-Group Calendar Events by Date** (~20 min) | [x] | [x] | `useMemo` builds `Map<string, Event[]>` keyed by `YYYY-MM-DD`. Per-cell `filter()` replaced with O(1) map lookup. |
|
||||||
| **Step 10: Add Dark Mode Toggle** (~20 min) | [x] | | `useTheme.ts` created (localStorage + prefers-color-scheme). Sun/moon toggle button added to `Header.tsx`. |
|
| **Step 10: Add Dark Mode Toggle** (~20 min) | [x] | [x] | `useTheme.ts` created (localStorage + prefers-color-scheme). Sun/moon toggle button added to `Header.tsx`. |
|
||||||
| **Step 11: Fix Bike Route Steps** (~5 min) | [x] | | `steps: "true"` already present in `BikeRoutingClient.getBikeRoute()` query params. |
|
| **Step 11: Fix Bike Route Steps** (~5 min) | [x] | [x] | `steps: "true"` already present in `BikeRoutingClient.getBikeRoute()` query params. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 4 — Monitoring & Testing (~1 hour)
|
## Phase 4 — Monitoring & Testing (~1 hour)
|
||||||
|
|
||||||
- [ ] **Step 12: Add Correlation IDs to API Errors** (~15 min)
|
| Step | ✅ Implemented | ✔️ Reviewed | Notes |
|
||||||
- Generate a short UUID (`randomUUID().slice(0, 8)`) in each API route's catch block; log it server-side and include it in the JSON error response
|
|------|:------:|:------:|-------|
|
||||||
- Files: all `src/app/api/*/route.ts`
|
| **Step 12: Add Correlation IDs to API Errors** (~15 min) | [x] | | `randomUUID().slice(0, 8)` in catch blocks of `bike-route`, `geocode`, `hafas`, `calendar`, `calendar/parse`. Logged server-side, returned in JSON. Existing API tests updated to assert `correlationId`. |
|
||||||
|
| **Step 13: Add Hook Tests** (~30 min) | [x] | | `useJourneys.test.ts` (4 tests: no-op when missing IDs, success, HTTP error, fetch throw). `useBikeRoute.test.ts` (4 tests: no-op when missing coords, success, HTTP error, fetch throw). |
|
||||||
- [ ] **Step 13: Add Hook Tests** (~30 min)
|
| **Step 14: Add Component Tests** (~15 min) | [x] | | `EventCard.test.tsx` (renders title + destination, hooks mocked). `CalendarView.test.tsx` (3 tests: month header, event on correct day, overflow indicator). |
|
||||||
- Create `useJourneys.test.ts` and `useBikeRoute.test.ts` — mock `global.fetch`, test state transitions (loading → success, loading → error)
|
|
||||||
- Files: `src/hooks/__tests__/useJourneys.test.ts`, `src/hooks/__tests__/useBikeRoute.test.ts`
|
|
||||||
|
|
||||||
- [ ] **Step 14: Add Component Tests** (~15 min)
|
|
||||||
- Create `EventCard.test.tsx` and `CalendarView.test.tsx` — render with mock data, verify key elements are in the document
|
|
||||||
- Files: `src/app/event/__tests__/EventCard.test.tsx`, `src/app/calendar/__tests__/CalendarView.test.tsx`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ describe("api/bike-route/route", () => {
|
|||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
expect(data).toEqual({ error: "Internal server error" });
|
expect(data.error).toBe("Internal server error");
|
||||||
|
expect(data.correlationId).toHaveLength(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle no route found", async () => {
|
it("should handle no route found", async () => {
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ describe("api/geocode/route", () => {
|
|||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
expect(data).toEqual({ error: "Internal server error" });
|
expect(data.error).toBe("Internal server error");
|
||||||
|
expect(data.correlationId).toHaveLength(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle no results found", async () => {
|
it("should handle no results found", async () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { randomUUID } from "crypto";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { BikeRoutingClient } from "@/lib/bike-routing-client";
|
import { BikeRoutingClient } from "@/lib/bike-routing-client";
|
||||||
|
|
||||||
@@ -27,7 +28,8 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
return NextResponse.json(route);
|
return NextResponse.json(route);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Bike route API error:", error);
|
const corrId = randomUUID().slice(0, 8);
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
console.error(`[${corrId}] Bike route API error:`, error);
|
||||||
|
return NextResponse.json({ error: "Internal server error", correlationId: corrId }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { randomUUID } from "crypto";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { extractEvents } from "@/lib/calendar-utils";
|
import { extractEvents } from "@/lib/calendar-utils";
|
||||||
import { DEFAULT_DAYS } from "@/lib/constants";
|
import { DEFAULT_DAYS } from "@/lib/constants";
|
||||||
@@ -15,7 +16,8 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
return NextResponse.json(events);
|
return NextResponse.json(events);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Calendar parse API error:", error);
|
const corrId = randomUUID().slice(0, 8);
|
||||||
return NextResponse.json({ error: "Failed to parse calendar" }, { status: 500 });
|
console.error(`[${corrId}] Calendar parse API error:`, error);
|
||||||
|
return NextResponse.json({ error: "Failed to parse calendar", correlationId: corrId }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { randomUUID } from "crypto";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { extractEvents } from "@/lib/calendar-utils";
|
import { extractEvents } from "@/lib/calendar-utils";
|
||||||
import { DEFAULT_DAYS } from "@/lib/constants";
|
import { DEFAULT_DAYS } from "@/lib/constants";
|
||||||
@@ -30,7 +31,8 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
return NextResponse.json(events);
|
return NextResponse.json(events);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Calendar API error:", error);
|
const corrId = randomUUID().slice(0, 8);
|
||||||
return NextResponse.json({ error: "Failed to fetch calendar" }, { status: 500 });
|
console.error(`[${corrId}] Calendar API error:`, error);
|
||||||
|
return NextResponse.json({ error: "Failed to fetch calendar", correlationId: corrId }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { randomUUID } from "crypto";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { GeocodingClient } from "@/lib/geocoding-client";
|
import { GeocodingClient } from "@/lib/geocoding-client";
|
||||||
|
|
||||||
@@ -22,7 +23,8 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
return NextResponse.json(results[0]);
|
return NextResponse.json(results[0]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Geocode API error:", error);
|
const corrId = randomUUID().slice(0, 8);
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
console.error(`[${corrId}] Geocode API error:`, error);
|
||||||
|
return NextResponse.json({ error: "Internal server error", correlationId: corrId }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { randomUUID } from "crypto";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { HAFAS_URL, HAFAS_TIMEOUT_MS } from "@/lib/constants";
|
import { HAFAS_URL, HAFAS_TIMEOUT_MS } from "@/lib/constants";
|
||||||
|
|
||||||
@@ -55,7 +56,8 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: "HAFAS request timeout" }, { status: 408 });
|
return NextResponse.json({ error: "HAFAS request timeout" }, { status: 408 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("HAFAS API error:", error);
|
const corrId = randomUUID().slice(0, 8);
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
console.error(`[${corrId}] HAFAS API error:`, error);
|
||||||
|
return NextResponse.json({ error: "Internal server error", correlationId: corrId }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import CalendarView from "../CalendarView";
|
||||||
|
|
||||||
|
describe("CalendarView", () => {
|
||||||
|
it("renders month header", () => {
|
||||||
|
render(
|
||||||
|
<CalendarView
|
||||||
|
events={[]}
|
||||||
|
onDateSelect={() => {}}
|
||||||
|
selectedDate={new Date("2025-06-15")}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("June 2025")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders events on the correct day", () => {
|
||||||
|
const event = {
|
||||||
|
id: "cal-1",
|
||||||
|
title: "Sprint Planning",
|
||||||
|
destination: "Vienna",
|
||||||
|
eventTime: new Date("2025-06-15T10:00:00"),
|
||||||
|
source: "calendar" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CalendarView
|
||||||
|
events={[event]}
|
||||||
|
onDateSelect={() => {}}
|
||||||
|
selectedDate={new Date("2025-06-15")}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("Sprint Planning")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows overflow indicator when more than 3 events on a day", () => {
|
||||||
|
const events = [
|
||||||
|
{ id: "1", title: "Event 1", destination: "A", eventTime: new Date("2025-06-10T09:00:00"), source: "manual" as const },
|
||||||
|
{ id: "2", title: "Event 2", destination: "B", eventTime: new Date("2025-06-10T10:00:00"), source: "manual" as const },
|
||||||
|
{ id: "3", title: "Event 3", destination: "C", eventTime: new Date("2025-06-10T11:00:00"), source: "manual" as const },
|
||||||
|
{ id: "4", title: "Event 4", destination: "D", eventTime: new Date("2025-06-10T12:00:00"), source: "manual" as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CalendarView
|
||||||
|
events={events}
|
||||||
|
onDateSelect={() => {}}
|
||||||
|
selectedDate={new Date("2025-06-10")}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("+1 more")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import EventCard from "../EventCard";
|
||||||
|
|
||||||
|
// Mock all hooks that EventCard depends on
|
||||||
|
vi.mock("@/hooks/useGeolocation", () => ({
|
||||||
|
useGeolocation: () => ({
|
||||||
|
location: null,
|
||||||
|
status: "pending",
|
||||||
|
requestLocation: () => {},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/hooks/useDestinationStation", () => ({
|
||||||
|
useDestinationStation: () => ({
|
||||||
|
station: { name: "Wien Hbf", extId: "0WB0F0001500" },
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/hooks/useGeocode", () => ({
|
||||||
|
useGeocode: () => ({
|
||||||
|
coords: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/hooks/useJourneys", () => ({
|
||||||
|
useJourneys: () => ({
|
||||||
|
journeys: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/hooks/useBikeRoute", () => ({
|
||||||
|
useBikeRoute: () => ({
|
||||||
|
bikeRoute: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/countdown-utils", () => ({
|
||||||
|
calculateCountdown: () => ({
|
||||||
|
label: "No deadline set",
|
||||||
|
color: "text-gray-400",
|
||||||
|
urgent: false,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("EventCard", () => {
|
||||||
|
it("renders event title and destination", () => {
|
||||||
|
const mockEvent = {
|
||||||
|
id: "test-1",
|
||||||
|
title: "Team Meeting",
|
||||||
|
destination: "Wien Hbf",
|
||||||
|
eventTime: new Date("2025-12-01T14:00:00"),
|
||||||
|
source: "manual" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockStation = { name: "Graz Hbf", extId: "0WB0F0000600" };
|
||||||
|
|
||||||
|
render(<EventCard event={mockEvent} originStation={mockStation} />);
|
||||||
|
expect(screen.getByText("Team Meeting")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Wien Hbf")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import { renderHook, waitFor } from "@testing-library/react";
|
||||||
|
import { useBikeRoute } from "../useBikeRoute";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("useBikeRoute", () => {
|
||||||
|
it("does nothing when coordinates are missing", async () => {
|
||||||
|
const { result } = renderHook(() => useBikeRoute(undefined, undefined, 48.21, 16.38));
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||||
|
expect(result.current.bikeRoute).toBeNull();
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns bike route when API succeeds", async () => {
|
||||||
|
vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
distance: 1500,
|
||||||
|
duration: 300,
|
||||||
|
steps: [
|
||||||
|
{ name: "Start", distance: 100, duration: 10, instruction: "Go straight" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useBikeRoute(48.2082, 16.3738, 48.21, 16.38),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||||
|
expect(result.current.bikeRoute).not.toBeNull();
|
||||||
|
expect(result.current.bikeRoute?.distance).toBe(1500);
|
||||||
|
expect(result.current.bikeRoute?.duration).toBe(300);
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets error when API fails", async () => {
|
||||||
|
vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useBikeRoute(48.2082, 16.3738, 48.21, 16.38),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.error).toBeTruthy());
|
||||||
|
expect(result.current.bikeRoute).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets error when fetch throws", async () => {
|
||||||
|
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("Network error"));
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useBikeRoute(48.2082, 16.3738, 48.21, 16.38),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.error).toBe("Network error"));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import { renderHook, waitFor } from "@testing-library/react";
|
||||||
|
import { useJourneys } from "../useJourneys";
|
||||||
|
|
||||||
|
// HAFAS response shape that parseHafasJourneys can process
|
||||||
|
function makeHafasSuccessData() {
|
||||||
|
return {
|
||||||
|
svcResL: [
|
||||||
|
{
|
||||||
|
res: {
|
||||||
|
outConL: [
|
||||||
|
{
|
||||||
|
ctxRecon: "journey-1",
|
||||||
|
secL: [
|
||||||
|
{
|
||||||
|
dep: { dTimeS: "20250101120000", dTimeR: "20250101120000" },
|
||||||
|
prod: { name: "Railjet 123" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arr: { aTimeS: "20250101130000", aTimeR: "20250101130000" },
|
||||||
|
prod: { name: "Railjet 123" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("useJourneys", () => {
|
||||||
|
it("does nothing when station IDs are missing", async () => {
|
||||||
|
const { result } = renderHook(() => useJourneys(null, "0WB0F0001500", new Date("2025-01-01T12:00:00")));
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||||
|
expect(result.current.journeys).toHaveLength(0);
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns journeys when API succeeds", async () => {
|
||||||
|
vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => makeHafasSuccessData(),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useJourneys("0WB0F0000600", "0WB0F0001500", new Date("2025-01-01T12:00:00")),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Briefly enters loading
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||||
|
expect(result.current.journeys).toHaveLength(1);
|
||||||
|
expect(result.current.journeys[0].id).toBe("journey-1");
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets error when API fails", async () => {
|
||||||
|
vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useJourneys("0WB0F0000600", "0WB0F0001500", new Date("2025-01-01T12:00:00")),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.error).toBeTruthy());
|
||||||
|
expect(result.current.journeys).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets error when fetch throws", async () => {
|
||||||
|
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("Network error"));
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useJourneys("0WB0F0000600", "0WB0F0001500", new Date("2025-01-01T12:00:00")),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.error).toBe("Network error"));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user