rewrite phase 7
This commit is contained in:
@@ -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" });
|
||||
});
|
||||
});
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 1–10 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 11–30 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 31–59 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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user