refactor: split calendar page components
This commit is contained in:
78
webui/tests/calendar-page-structure.test.mjs
Normal file
78
webui/tests/calendar-page-structure.test.mjs
Normal file
@@ -0,0 +1,78 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
const page = fs.readFileSync(
|
||||
new URL(
|
||||
"../src/features/calendar/CalendarPage.tsx",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const views = fs.readFileSync(
|
||||
new URL(
|
||||
"../src/features/calendar/CalendarViews.tsx",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const model = fs.readFileSync(
|
||||
new URL(
|
||||
"../src/features/calendar/calendarViewModel.ts",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const collectionDialogs = fs.readFileSync(
|
||||
new URL(
|
||||
"../src/features/calendar/CalendarCollectionDialogs.tsx",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const eventDialog = fs.readFileSync(
|
||||
new URL(
|
||||
"../src/features/calendar/CalendarEventDialog.tsx",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert(
|
||||
page.includes('from "./CalendarViews"'),
|
||||
"CalendarPage must compose the focused calendar view components",
|
||||
);
|
||||
assert(
|
||||
!page.includes("function CalendarWeekRows("),
|
||||
"CalendarPage must not own month/continuous rendering",
|
||||
);
|
||||
assert(
|
||||
!page.includes("function CalendarTimeGrid("),
|
||||
"CalendarPage must not own time-grid rendering",
|
||||
);
|
||||
assert(
|
||||
views.includes("export function CalendarWeekRows(") &&
|
||||
views.includes("export function CalendarTimeGrid("),
|
||||
"CalendarViews must own both view families",
|
||||
);
|
||||
assert(
|
||||
model.includes("export function layoutTimedEventsForDay(") &&
|
||||
model.includes("export function continuousVirtualWindow("),
|
||||
"calendar layout and virtualization must remain independently testable",
|
||||
);
|
||||
assert(
|
||||
!page.includes("function CalendarCollectionDialog(") &&
|
||||
!page.includes("function CalendarEventDialog("),
|
||||
"CalendarPage must not own source or event form dialogs",
|
||||
);
|
||||
assert(
|
||||
collectionDialogs.includes(
|
||||
"export function CalendarCollectionDialog(",
|
||||
) &&
|
||||
eventDialog.includes("export function CalendarEventDialog("),
|
||||
"calendar dialogs must remain focused components",
|
||||
);
|
||||
|
||||
console.log("Calendar page decomposition checks passed.");
|
||||
96
webui/tests/calendar-view-model.test.ts
Normal file
96
webui/tests/calendar-view-model.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
continuousVirtualWindow,
|
||||
groupEventsByDay,
|
||||
layoutTimedEventsForDay,
|
||||
moveEventToDay,
|
||||
rangeForMode,
|
||||
resizeEventToTime,
|
||||
} from "../src/features/calendar/calendarViewModel.ts";
|
||||
|
||||
function assert(condition: unknown, message: string): void {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function event(
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
allDay = false,
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
calendar_id: "calendar-1",
|
||||
summary: id,
|
||||
start_at: start.toISOString(),
|
||||
end_at: end.toISOString(),
|
||||
all_day: allDay,
|
||||
} as never;
|
||||
}
|
||||
|
||||
const focus = new Date(2026, 6, 15, 12);
|
||||
const month = rangeForMode("month", focus, {
|
||||
before: 8,
|
||||
after: 12,
|
||||
});
|
||||
assert(
|
||||
(month.end.getTime() - month.start.getTime()) / 86_400_000 === 42,
|
||||
"month view should retain its six-week window",
|
||||
);
|
||||
|
||||
const virtual = continuousVirtualWindow(
|
||||
100,
|
||||
{ scrollTop: 20 * 92, height: 8 * 92 },
|
||||
3,
|
||||
);
|
||||
assert(virtual.start === 17, "virtualization should retain overscan above");
|
||||
assert(virtual.end === 31, "virtualization should retain overscan below");
|
||||
assert(
|
||||
virtual.topSpacerHeight + virtual.bottomSpacerHeight > 0,
|
||||
"virtualization should preserve offscreen geometry",
|
||||
);
|
||||
|
||||
const day = new Date(2026, 6, 7);
|
||||
const parallel = [0, 1, 2, 3].map((index) =>
|
||||
event(
|
||||
`event-${index}`,
|
||||
new Date(2026, 6, 7, 9),
|
||||
new Date(2026, 6, 7, 10),
|
||||
),
|
||||
);
|
||||
const layout = layoutTimedEventsForDay(parallel, day);
|
||||
assert(
|
||||
layout.items.length === 2 && layout.overflows.length === 1,
|
||||
"parallel events should use bounded columns plus one overflow control",
|
||||
);
|
||||
assert(
|
||||
layout.overflows[0].events.length === 2,
|
||||
"overflow should retain every hidden event",
|
||||
);
|
||||
|
||||
const allDay = event(
|
||||
"all-day",
|
||||
new Date(2026, 6, 7),
|
||||
new Date(2026, 6, 9),
|
||||
true,
|
||||
);
|
||||
const grouped = groupEventsByDay([allDay]);
|
||||
assert(grouped.size === 2, "all-day end dates should remain exclusive");
|
||||
const moved = moveEventToDay(allDay, new Date(2026, 6, 20));
|
||||
assert(
|
||||
moved.endAt?.getDate() === 22,
|
||||
"moving an all-day event should preserve its duration",
|
||||
);
|
||||
|
||||
const timed = event(
|
||||
"timed",
|
||||
new Date(2026, 6, 7, 10),
|
||||
new Date(2026, 6, 7, 11),
|
||||
);
|
||||
const resized = resizeEventToTime(timed, "end", day, 9 * 60);
|
||||
assert(
|
||||
resized.endAt !== null &&
|
||||
resized.endAt.getTime() > resized.startAt.getTime(),
|
||||
"resizing must not invert an event",
|
||||
);
|
||||
|
||||
console.log("Calendar view model interaction checks passed.");
|
||||
Reference in New Issue
Block a user