Some checks failed
CI / lint-and-test (push) Failing after 45s
- Added Node.js setup and webapp testing steps to the CI workflow for improved integration. - Updated HTML to link multiple CSS files for better modularity and organization of styles. - Removed deprecated `style.css` and introduced new CSS files for base styles, calendar, day detail, hints, markers, states, and duty list to enhance maintainability and readability. - Implemented new styles for improved presentation of duty information and user interactions. - Added unit tests for new API functions and contact link rendering to ensure functionality and reliability.
140 lines
4.6 KiB
JavaScript
140 lines
4.6 KiB
JavaScript
/**
|
|
* Unit tests for calendar: dutiesByDate (including edge case end_at < start_at),
|
|
* calendarEventsByDate.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, beforeEach } from "vitest";
|
|
|
|
beforeAll(() => {
|
|
document.body.innerHTML =
|
|
'<div id="calendar"></div><h2 id="monthTitle"></h2>' +
|
|
'<div id="dutyList"></div><div id="loading"></div><div id="error"></div>' +
|
|
'<div id="accessDenied"></div><div class="header"></div><div class="weekdays"></div>' +
|
|
'<button id="prevMonth"></button><button id="nextMonth"></button>';
|
|
});
|
|
|
|
import { dutiesByDate, calendarEventsByDate, renderCalendar } from "./calendar.js";
|
|
import { state } from "./dom.js";
|
|
|
|
describe("dutiesByDate", () => {
|
|
it("groups duty by single local day", () => {
|
|
const duties = [
|
|
{
|
|
full_name: "Alice",
|
|
start_at: "2025-02-25T09:00:00Z",
|
|
end_at: "2025-02-25T18:00:00Z",
|
|
},
|
|
];
|
|
const byDate = dutiesByDate(duties);
|
|
expect(byDate["2025-02-25"]).toHaveLength(1);
|
|
expect(byDate["2025-02-25"][0].full_name).toBe("Alice");
|
|
});
|
|
|
|
it("spans duty across multiple days", () => {
|
|
const duties = [
|
|
{
|
|
full_name: "Bob",
|
|
start_at: "2025-02-25T00:00:00Z",
|
|
end_at: "2025-02-27T23:59:59Z",
|
|
},
|
|
];
|
|
const byDate = dutiesByDate(duties);
|
|
const keys = Object.keys(byDate).sort();
|
|
expect(keys.length).toBeGreaterThanOrEqual(2);
|
|
keys.forEach((k) => expect(byDate[k]).toHaveLength(1));
|
|
expect(byDate[keys[0]][0].full_name).toBe("Bob");
|
|
});
|
|
|
|
it("skips duty when end_at < start_at (no infinite loop)", () => {
|
|
const duties = [
|
|
{
|
|
full_name: "Bad",
|
|
start_at: "2025-02-28T12:00:00Z",
|
|
end_at: "2025-02-25T08:00:00Z",
|
|
},
|
|
];
|
|
const byDate = dutiesByDate(duties);
|
|
expect(Object.keys(byDate)).toHaveLength(0);
|
|
});
|
|
|
|
it("does not iterate more than MAX_DAYS_PER_DUTY", () => {
|
|
const start = "2025-01-01T00:00:00Z";
|
|
const end = "2026-06-01T00:00:00Z";
|
|
const duties = [{ full_name: "Long", start_at: start, end_at: end }];
|
|
const byDate = dutiesByDate(duties);
|
|
const keys = Object.keys(byDate).sort();
|
|
expect(keys.length).toBeLessThanOrEqual(367);
|
|
});
|
|
|
|
it("handles empty duties", () => {
|
|
expect(dutiesByDate([])).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe("calendarEventsByDate", () => {
|
|
it("maps events to local date key by UTC date", () => {
|
|
const events = [
|
|
{ date: "2025-02-25", summary: "Holiday" },
|
|
{ date: "2025-02-25", summary: "Meeting" },
|
|
{ date: "2025-02-26", summary: "Other" },
|
|
];
|
|
const byDate = calendarEventsByDate(events);
|
|
expect(byDate["2025-02-25"]).toEqual(["Holiday", "Meeting"]);
|
|
expect(byDate["2025-02-26"]).toEqual(["Other"]);
|
|
});
|
|
|
|
it("skips events without summary", () => {
|
|
const events = [{ date: "2025-02-25", summary: null }];
|
|
const byDate = calendarEventsByDate(events);
|
|
expect(byDate["2025-02-25"] || []).toHaveLength(0);
|
|
});
|
|
|
|
it("handles null or undefined events", () => {
|
|
expect(calendarEventsByDate(null)).toEqual({});
|
|
expect(calendarEventsByDate(undefined)).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe("renderCalendar", () => {
|
|
beforeEach(() => {
|
|
state.lang = "en";
|
|
});
|
|
|
|
it("renders 42 cells (6 weeks)", () => {
|
|
renderCalendar(2025, 0, {}, {});
|
|
const calendarEl = document.getElementById("calendar");
|
|
const cells = calendarEl?.querySelectorAll(".day") ?? [];
|
|
expect(cells.length).toBe(42);
|
|
});
|
|
|
|
it("sets data-date on each cell to YYYY-MM-DD", () => {
|
|
renderCalendar(2025, 0, {}, {});
|
|
const calendarEl = document.getElementById("calendar");
|
|
const cells = Array.from(calendarEl?.querySelectorAll(".day") ?? []);
|
|
const dates = cells.map((c) => c.getAttribute("data-date"));
|
|
expect(dates.every((d) => /^\d{4}-\d{2}-\d{2}$/.test(d ?? ""))).toBe(true);
|
|
});
|
|
|
|
it("adds today class to cell matching today", () => {
|
|
const today = new Date();
|
|
renderCalendar(today.getFullYear(), today.getMonth(), {}, {});
|
|
const calendarEl = document.getElementById("calendar");
|
|
const todayKey =
|
|
today.getFullYear() +
|
|
"-" +
|
|
String(today.getMonth() + 1).padStart(2, "0") +
|
|
"-" +
|
|
String(today.getDate()).padStart(2, "0");
|
|
const todayCell = calendarEl?.querySelector('.day.today[data-date="' + todayKey + '"]');
|
|
expect(todayCell).toBeTruthy();
|
|
});
|
|
|
|
it("sets month title from state.lang and given year/month", () => {
|
|
state.lang = "en";
|
|
renderCalendar(2025, 1, {}, {});
|
|
const titleEl = document.getElementById("monthTitle");
|
|
expect(titleEl?.textContent).toContain("2025");
|
|
expect(titleEl?.textContent).toContain("February");
|
|
});
|
|
});
|