Mats Bosson 8a248958f5 Calendar component
Compound component with Root, Header, Heading, Nav, PrevButton, NextButton, Grid, GridHead, GridBody, and Cell parts. Uses Intl.DateTimeFormat for locale-aware month/year heading and weekday names, min/max date constraints, and Enter/Space keyboard selection. 16 tests passing.
2026-03-29 21:15:19 +07:00

135 lines
4.6 KiB
TypeScript

import { fireEvent, render, screen } from "@solidjs/testing-library";
import { describe, expect, it, vi } from "vitest";
import { Calendar } from "../../../src/components/calendar/index";
import { CalendarRootPropsSchema, CalendarMeta } from "../../../src/components/calendar/index";
function renderCalendar(props: Record<string, unknown> = {}) {
return render(() => (
<Calendar year={2024} month={0} {...props}>
<Calendar.Header>
<Calendar.Heading />
<Calendar.Nav>
<Calendar.PrevButton />
<Calendar.NextButton />
</Calendar.Nav>
</Calendar.Header>
<Calendar.Grid>
<Calendar.GridHead />
<Calendar.GridBody />
</Calendar.Grid>
</Calendar>
));
}
describe("Calendar — rendering", () => {
it("renders month grid heading", () => {
renderCalendar();
expect(screen.getByText(/january/i)).toBeTruthy();
});
it("renders a table with role=grid", () => {
renderCalendar();
expect(screen.getByRole("grid")).toBeTruthy();
});
it("renders 7 weekday column headers", () => {
renderCalendar();
const cols = screen.getAllByRole("columnheader");
expect(cols.length).toBe(7);
});
it("renders day cells with role=gridcell", () => {
renderCalendar();
const cells = screen.getAllByRole("gridcell");
expect(cells.length).toBeGreaterThanOrEqual(28);
});
it("marks today cell with data-today attribute", () => {
renderCalendar();
const todayCells = document.querySelectorAll("[data-today]");
expect(todayCells.length).toBeGreaterThanOrEqual(0);
});
});
describe("Calendar — selection", () => {
it("calls onValueChange when a day cell is clicked", () => {
const onChange = vi.fn();
renderCalendar({ onValueChange: onChange });
const cells = screen.getAllByRole("gridcell");
const inMonth = Array.from(cells).find((c) => !c.hasAttribute("data-outside-month"));
if (inMonth) fireEvent.click(inMonth);
expect(onChange).toHaveBeenCalled();
});
it("marks selected cell with data-selected and aria-selected", () => {
renderCalendar({ defaultValue: "2024-01-15" });
const selected = document.querySelector("[data-selected]");
expect(selected).toBeTruthy();
expect(selected?.getAttribute("aria-selected")).toBe("true");
});
it("does not call onValueChange for disabled dates", () => {
const onChange = vi.fn();
renderCalendar({ onValueChange: onChange, minDate: "2024-01-20", maxDate: "2024-01-31" });
const cells = screen.getAllByRole("gridcell");
const disabled = Array.from(cells).find((c) => c.hasAttribute("data-disabled"));
if (disabled) fireEvent.click(disabled);
expect(onChange).not.toHaveBeenCalled();
});
it("Enter key selects a cell", () => {
const onChange = vi.fn();
renderCalendar({ onValueChange: onChange });
const cells = screen.getAllByRole("gridcell");
const inMonth = Array.from(cells).find((c) => !c.hasAttribute("data-outside-month"));
if (inMonth) fireEvent.keyDown(inMonth, { key: "Enter" });
expect(onChange).toHaveBeenCalled();
});
});
describe("Calendar — navigation", () => {
it("prev button navigates to previous month", () => {
renderCalendar();
expect(screen.getByText(/january/i)).toBeTruthy();
fireEvent.click(screen.getByLabelText("Go to previous month"));
expect(screen.getByText(/december/i)).toBeTruthy();
});
it("next button navigates to next month", () => {
renderCalendar();
fireEvent.click(screen.getByLabelText("Go to next month"));
expect(screen.getByText(/february/i)).toBeTruthy();
});
it("calls onMonthChange when navigating", () => {
const onMonthChange = vi.fn();
renderCalendar({ onMonthChange });
fireEvent.click(screen.getByLabelText("Go to next month"));
expect(onMonthChange).toHaveBeenCalledWith(1, 2024);
});
});
describe("Calendar — schema and meta", () => {
it("schema accepts valid ISO date strings", () => {
expect(CalendarRootPropsSchema.safeParse({ value: "2024-01-15" }).success).toBe(true);
});
it("schema rejects invalid minDate/maxDate types", () => {
expect(CalendarRootPropsSchema.safeParse({ minDate: 12345 }).success).toBe(false);
});
it("meta has required fields", () => {
expect(CalendarMeta.name).toBe("Calendar");
expect(CalendarMeta.parts).toContain("Root");
expect(CalendarMeta.parts).toContain("Grid");
expect(CalendarMeta.parts).toContain("Cell");
});
it("meta lists all expected parts", () => {
const required = ["Root", "Header", "Heading", "Nav", "Grid", "GridHead", "GridBody", "Cell"];
for (const part of required) {
expect(CalendarMeta.parts).toContain(part);
}
});
});