Hapio Calendar

Testing

@hapio/calendar-react/testing and @hapio/calendar-core/testing: conformance, the contract checker, rendering and clocks.

See Testing for how to use them.

Conformance

function runConformance(render: () => ReactNode, o?: ConformanceOptions): Promise<ConformanceReport>;

Mounts render() (renderCalendar), then checks the markup against the accessibility contract in every view: the contract checker's rules (one tab stop per composite, role nesting, resolving aria-* references, the live region, chronological event order, unique ids, the slot cursor, the selection state and the editing parts), accessible names (events time first; day cells; background events with labels), the keyboard maps of the composites (dispatched keydown events; focus follows; the slot cursor is re-keyed per slot), at most three tab stops per view, keyboard selection where it is on (Space starts, an arrow key extends, Escape cancels; each step is drawn, exposed and announced; nothing is confirmed), keyboard editing where an event can be edited (Space grabs it without a click or a scroll and keeps focus, an arrow key draws its mirror, Escape cancels; each step is announced; nothing is dropped), a mouse drag of an event in the time grids (its mirror and the drag preview appear, Escape cancels before the release; nothing is dropped), the event actions where a view renders them (Shift+F10 on an event opens a named menu of menu items with focus on the first, Escape closes it with focus back on the event; "Move to…" starts placing with focus on the slot cursor or the day cell, an arrow key moves it and draws the event's mirror, Escape cancels with focus back on the event; announced; nothing is committed), and axe with zero disabled rules when o.axe is given. Unmounts afterwards. Rejects with an Error listing every failure; resolves with the report otherwise.

Each failure names its suite, rule and view and links https://calendar.hapio.io/docs/a11y-contract#<rule>. The "+N more" popover is opened (its first trigger of each variant per view): it must be a named dialog that takes focus, closes on Escape and on its close button, and returns focus to its trigger. Keys are dispatched as events, so nothing here depends on a test runner; run it in a real browser (Vitest browser mode), since names, visibility and layout come from the DOM. The selection suite reads announcements from the live region, so render it with the default announcer ('auto' speaks through ariaNotify where the browser has it, and the region stays empty).

import axe from 'axe-core'
it('my calendar conforms', () => runConformance(() => <MyCalendar />, { axe: (el) => axe.run(el) }))

Options of runConformance.

Prop

Type

The result of a passing runConformance.

Prop

Type

One failed check.

Prop

Type

type ConformanceSuite = "roles" | "names" | "focus" | "keyboard" | "selection" | "grab" | "drag" | "actions" | "axe";
type AxeRunner = (context: Element) => Promise<AxeResultsLike>;

Runs axe on an element, e.g. (element) => axe.run(element) with axe-core's default export.

function expectNoAxeViolations(container: Element, axe: AxeRunner): Promise<void>;

Runs axe on container and rejects with an Error listing every violation (rule id, help and node count). No rule is disabled.

await expectNoAxeViolations(calendar.container, (el) => axe.run(el))

The contract checker

function checkContract(root: Element, o?: CheckContractOptions): readonly ContractViolation[];

Checks the markup under root against the v0 rules and returns every violation (empty when clean). Reads the DOM only (and getComputedStyle for live-region); never mutates it.

What checkContract checks against.

Prop

Type

One finding.

Prop

Type

type ContractRule = "composite-tab-stop" | "role-nesting" | "aria-reference" | "live-region" | "chronological-order" | "duplicate-id" | "time-surface-padding" | "slot-cursor" | "selection-state" | "edit-parts" | "event-actions";

The v0 rules. Each warning links to https://calendar.hapio.io/docs/a11y-contract#<rule>.

Rendering

function renderCalendar(ui: ReactNode, o?: RenderCalendarOptions): Promise<RenderedCalendar>;

Mounts ui with react-dom/client inside act (so the store has connected, the first sync has run and the clock is known when it resolves) and returns the container. Works with the registry block, a CalendarProvider tree, or custom markup on the hooks; Vitest browser mode is the supported runner.

Resolves once the calendar has settled (its root is rendered and not pending, and a day grid's 'auto' capacity is measured), or after three seconds with whatever rendered, so a test can report what it finds (root is null without a calendar root).

const calendar = await renderCalendar(<HapioCalendar events={events} timeZone="UTC" />)
expect(calendar.root?.getAttribute('data-view')).toBe('week')
calendar.unmount()

Options of renderCalendar.

Prop

Type

A mounted calendar (renderCalendar).

Prop

Type

Clocks and data (@hapio/calendar-core/testing)

function fixedClock(at: DateInput): ClockSource;

A clock stopped at at (an instant, or a floating/all-day input read as UTC). Its subscriptions never tick.

function manualClock(at: DateInput): ManualClock;

A manual clock starting at at.

function generateEvents(o: GenerateEventsOptions): CalendarEventInput[];

Deterministic events (same options, same output) for benches and property tests. Ids are e0, e1, …

function loadDstVectors(json: unknown): readonly DstVector[];

Validates parsed JSON against DstVectorFile and returns its vectors. Throws a TypeError naming the first invalid path.