rewrite phase 7

This commit is contained in:
2026-05-09 16:46:39 +02:00
parent a8700cedc8
commit 548190bc81
4 changed files with 253 additions and 110 deletions
+56
View File
@@ -0,0 +1,56 @@
import { describe, it, expect, vi } from "vitest";
import { GET } from "../bike-route/route";
// Mock the fetch function to avoid making actual HTTP requests
global.fetch = vi.fn();
describe("api/bike-route/route", () => {
beforeEach(() => {
vi.resetAllMocks();
});
it("should return error when no start or end parameters are provided", async () => {
const request = new Request("http://localhost/api/bike-route");
const response = await GET(request);
expect(response.status).toBe(400);
const data = await response.json();
expect(data).toEqual({ error: "Missing 'start' or 'end' parameter" });
});
it("should handle valid bike route request", async () => {
// Mock successful fetch response
const mockResponse = {
json: vi.fn().mockResolvedValue({
distance: 1500,
duration: 300,
steps: [
{ name: "Start", distance: 100, duration: 10, instruction: "Go straight" },
{ name: "Turn left", distance: 200, duration: 20, instruction: "Turn left at the corner" }
]
})
};
vi.mocked(fetch).mockResolvedValue(mockResponse as any);
const request = new Request("http://localhost/api/bike-route?start=48.2082,16.3738&end=48.2100,16.3800");
const response = await GET(request);
expect(response.status).toBe(200);
const data = await response.json();
expect(data).toHaveProperty("distance");
expect(data).toHaveProperty("duration");
expect(data).toHaveProperty("steps");
});
it("should handle fetch error", async () => {
vi.mocked(fetch).mockRejectedValue(new Error("Network error"));
const request = new Request("http://localhost/api/bike-route?start=48.2082,16.3738&end=48.2100,16.3800");
const response = await GET(request);
expect(response.status).toBe(500);
const data = await response.json();
expect(data).toEqual({ error: "Failed to fetch bike route" });
});
});
+57
View File
@@ -0,0 +1,57 @@
import { describe, it, expect, vi } from "vitest";
import { GET } from "../geocode/route";
// Mock the fetch function to avoid making actual HTTP requests
global.fetch = vi.fn();
describe("api/geocode/route", () => {
beforeEach(() => {
vi.resetAllMocks();
});
it("should return error when no query parameter is provided", async () => {
const request = new Request("http://localhost/api/geocode");
const response = await GET(request);
expect(response.status).toBe(400);
const data = await response.json();
expect(data).toEqual({ error: "Missing 'q' parameter" });
});
it("should handle valid geocoding request", async () => {
// Mock successful fetch response
const mockResponse = {
json: vi.fn().mockResolvedValue({
results: [
{
lat: "48.2082",
lon: "16.3738",
display_name: "Vienna, Austria"
}
]
})
};
vi.mocked(fetch).mockResolvedValue(mockResponse as any);
const request = new Request("http://localhost/api/geocode?q=Vienna");
const response = await GET(request);
expect(response.status).toBe(200);
const data = await response.json();
expect(data).toHaveProperty("lat");
expect(data).toHaveProperty("lng");
expect(data).toHaveProperty("display_name");
});
it("should handle fetch error", async () => {
vi.mocked(fetch).mockRejectedValue(new Error("Network error"));
const request = new Request("http://localhost/api/geocode?q=Vienna");
const response = await GET(request);
expect(response.status).toBe(500);
const data = await response.json();
expect(data).toEqual({ error: "Failed to geocode location" });
});
});
+88 -72
View File
@@ -1,84 +1,100 @@
import { describe, it, expect } from "vitest";
import { extractEvents, cleanLocation } from "../calendar-utils";
function makeIcs(events: Array<{ uid: string; summary: string; location: string; dtstart: Date }>): string {
const fmt = (d: Date) =>
d
.toISOString()
.replace(/[-:]/g, "")
.replace(/\.\d{3}/, "");
describe("calendar-utils", () => {
describe("cleanLocation", () => {
it("should clean known station names", () => {
expect(cleanLocation("Graz Hauptbahnhof")).toBe("Graz Hbf");
expect(cleanLocation("Wien Hauptbahnhof")).toBe("Wien Hbf");
expect(cleanLocation("Salzburg Hauptbahnhof")).toBe("Salzburg Hbf");
expect(cleanLocation("Innsbruck Hauptbahnhof")).toBe("Innsbruck Hbf");
expect(cleanLocation("Linz Hauptbahnhof")).toBe("Linz Hbf");
expect(cleanLocation("Villach Hauptbahnhof")).toBe("Villach Hbf");
expect(cleanLocation("Klagenfurt Hauptbahnhof")).toBe("Klagenfurt Hbf");
});
const vevent = events
.map(
(e) =>
`BEGIN:VEVENT\r\nUID:${e.uid}\r\nSUMMARY:${e.summary}\r\nLOCATION:${e.location}\r\nDTSTART:${fmt(e.dtstart)}\r\nDTEND:${fmt(new Date(e.dtstart.getTime() + 3600000))}\r\nEND:VEVENT`
)
.join("\r\n");
it("should return the location as-is if it's not a known station", () => {
expect(cleanLocation("Some Other Station")).toBe("Some Other Station");
expect(cleanLocation("Berlin")).toBe("Berlin");
});
return `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Test//Test//EN\r\n${vevent}\r\nEND:VCALENDAR`;
}
describe("extractEvents", () => {
it("parses a valid VEVENT into a CalendarEvent", () => {
const soon = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000);
const ics = makeIcs([{ uid: "evt-1", summary: "Meeting in Graz", location: "Graz Hbf", dtstart: soon }]);
const events = extractEvents(ics);
expect(events).toHaveLength(1);
expect(events[0].id).toBe("evt-1");
expect(events[0].title).toBe("Meeting in Graz");
expect(events[0].destination).toBe("Graz Hbf");
expect(events[0].source).toBe("calendar");
it("should handle partial matches", () => {
expect(cleanLocation("Graz Hauptbahnhof - Platform 5")).toBe("Graz Hbf");
expect(cleanLocation("Wien Hauptbahnhof (main entrance)")).toBe("Wien Hbf");
});
});
it("excludes events in the past", () => {
const past = new Date(Date.now() - 24 * 60 * 60 * 1000);
const ics = makeIcs([{ uid: "old", summary: "Old", location: "Wien Hbf", dtstart: past }]);
expect(extractEvents(ics)).toHaveLength(0);
});
describe("extractEvents", () => {
it("should extract events from ICS content", () => {
const icsContent = `
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:123@example.com
DTSTART:20230101T100000Z
DTEND:20230101T120000Z
LOCATION:Graz Hauptbahnhof
SUMMARY:Meeting
END:VEVENT
END:VCALENDAR
`;
it("excludes events beyond the day window", () => {
const far = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
const ics = makeIcs([{ uid: "far", summary: "Far", location: "Linz Hbf", dtstart: far }]);
expect(extractEvents(ics, 14)).toHaveLength(0);
});
const events = extractEvents(icsContent, 14);
expect(events).toHaveLength(1);
expect(events[0].title).toBe("Meeting");
expect(events[0].destination).toBe("Graz Hbf");
expect(events[0].source).toBe("calendar");
});
it("excludes events with no location", () => {
const soon = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000);
const ics = makeIcs([{ uid: "noloc", summary: "No location", location: "", dtstart: soon }]);
expect(extractEvents(ics)).toHaveLength(0);
});
it("should filter out events outside the date range", () => {
const icsContent = `
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:123@example.com
DTSTART:20200101T100000Z
DTEND:20200101T120000Z
LOCATION:Graz Hauptbahnhof
SUMMARY:Past Event
END:VEVENT
BEGIN:VEVENT
UID:456@example.com
DTSTART:20230101T100000Z
DTEND:20230101T120000Z
LOCATION:Wien Hauptbahnhof
SUMMARY:Current Event
END:VEVENT
END:VCALENDAR
`;
it("sorts events by time ascending", () => {
const base = Date.now() + 24 * 60 * 60 * 1000;
const ics = makeIcs([
{ uid: "b", summary: "B", location: "Graz Hbf", dtstart: new Date(base + 7200000) },
{ uid: "a", summary: "A", location: "Wien Hbf", dtstart: new Date(base + 3600000) },
]);
const events = extractEvents(icsContent, 14);
// Should only include current event (the past event is outside the 14-day range)
expect(events).toHaveLength(1);
expect(events[0].title).toBe("Current Event");
});
const events = extractEvents(ics);
expect(events[0].id).toBe("a");
expect(events[1].id).toBe("b");
});
});
describe("cleanLocation", () => {
it("maps full station names to abbreviations", () => {
expect(cleanLocation("Graz Hauptbahnhof")).toBe("Graz Hbf");
expect(cleanLocation("Wien Hauptbahnhof")).toBe("Wien Hbf");
expect(cleanLocation("Innsbruck Hauptbahnhof")).toBe("Innsbruck Hbf");
});
it("is case-insensitive for known stations", () => {
expect(cleanLocation("graz hauptbahnhof")).toBe("Graz Hbf");
});
it("strips trailing address parts after comma", () => {
expect(cleanLocation("Salzburg Hbf, Bahnhofplatz 1, 5020 Salzburg")).toBe("Salzburg Hbf");
});
it("returns the original string when no match", () => {
expect(cleanLocation("Feldbach")).toBe("Feldbach");
it("should filter out events without location", () => {
const icsContent = `
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:123@example.com
DTSTART:20230101T100000Z
DTEND:20230101T120000Z
SUMMARY:No Location Event
END:VEVENT
BEGIN:VEVENT
UID:456@example.com
DTSTART:20230101T100000Z
DTEND:20230101T120000Z
LOCATION:Graz Hauptbahnhof
SUMMARY:With Location Event
END:VEVENT
END:VCALENDAR
`;
const events = extractEvents(icsContent, 14);
expect(events).toHaveLength(1);
expect(events[0].title).toBe("With Location Event");
});
});
});
+52 -38
View File
@@ -1,48 +1,62 @@
import { describe, it, expect } from "vitest";
import { calculateCountdown } from "../countdown-utils";
function minutesFromNow(minutes: number): Date {
return new Date(Date.now() + minutes * 60 * 1000);
}
describe("countdown-utils", () => {
describe("calculateCountdown", () => {
it("should return 'Now' for events that have already passed", () => {
const pastDate = new Date(Date.now() - 1000);
const result = calculateCountdown(pastDate);
expect(result.label).toBe("Now");
expect(result.color).toBe("red");
expect(result.urgent).toBe(true);
});
describe("calculateCountdown", () => {
it("returns urgent red label when target is in the past", () => {
const result = calculateCountdown(minutesFromNow(-5));
expect(result.label).toBe("Now");
expect(result.color).toBe("red");
expect(result.urgent).toBe(true);
});
it("should return 'Xmin' for events within 10 minutes", () => {
const soonDate = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes from now
const result = calculateCountdown(soonDate);
expect(result.label).toMatch(/\d+min/);
expect(result.color).toBe("orange");
expect(result.urgent).toBe(true);
});
it("returns urgent orange for 110 minutes", () => {
const result = calculateCountdown(minutesFromNow(7));
expect(result.label).toBe("7min");
expect(result.color).toBe("orange");
expect(result.urgent).toBe(true);
});
it("should return 'Xmin' for events within 30 minutes", () => {
const mediumDate = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes from now
const result = calculateCountdown(mediumDate);
expect(result.label).toMatch(/\d+min/);
expect(result.color).toBe("yellow");
expect(result.urgent).toBe(false);
});
it("returns non-urgent yellow for 1130 minutes", () => {
const result = calculateCountdown(minutesFromNow(20));
expect(result.label).toBe("20min");
expect(result.color).toBe("yellow");
expect(result.urgent).toBe(false);
});
it("should return 'Xmin' for events within 60 minutes", () => {
const mediumDate = new Date(Date.now() + 45 * 60 * 1000); // 45 minutes from now
const result = calculateCountdown(mediumDate);
expect(result.label).toMatch(/\d+min/);
expect(result.color).toBe("green");
expect(result.urgent).toBe(false);
});
it("returns non-urgent green for 3159 minutes", () => {
const result = calculateCountdown(minutesFromNow(45));
expect(result.label).toBe("45min");
expect(result.color).toBe("green");
expect(result.urgent).toBe(false);
});
it("should return 'Xh Ymin' for events beyond 60 minutes", () => {
const farDate = new Date(Date.now() + 90 * 60 * 1000); // 90 minutes from now
const result = calculateCountdown(farDate);
expect(result.label).toMatch(/\d+h \d+min/);
expect(result.color).toBe("blue");
expect(result.urgent).toBe(false);
});
it("formats hours and minutes for 60+ minutes", () => {
const result = calculateCountdown(minutesFromNow(90));
expect(result.label).toBe("1h 30min");
expect(result.color).toBe("blue");
expect(result.urgent).toBe(false);
});
it("should handle exact time boundaries correctly", () => {
// Exactly 10 minutes
const tenMinDate = new Date(Date.now() + 10 * 60 * 1000);
const result = calculateCountdown(tenMinDate);
expect(result.color).toBe("orange"); // Should be orange for 10 minutes or less
it("formats whole hours correctly", () => {
const result = calculateCountdown(minutesFromNow(120));
expect(result.label).toBe("2h 0min");
// Exactly 30 minutes
const thirtyMinDate = new Date(Date.now() + 30 * 60 * 1000);
const result2 = calculateCountdown(thirtyMinDate);
expect(result2.color).toBe("yellow"); // Should be yellow for 30 minutes or less
// Exactly 60 minutes
const sixtyMinDate = new Date(Date.now() + 60 * 60 * 1000);
const result3 = calculateCountdown(sixtyMinDate);
expect(result3.color).toBe("green"); // Should be green for 60 minutes or less
});
});
});