How to Test Calendar Booking and Scheduling
Short answer
Scheduling UIs fail on timezone-shifted slots, race double-bookings, buffer rules, and resource capacity—not on whether a calendar renders. Seed slot inventory per runId, use Playwright clock for same-day boundaries, probe booking rows for exclusivity, and test parallel booking conflicts explicitly.
Part of Testing Guides by UI patterns.
Who this is for
Teams shipping appointment booking, meeting schedulers, resource calendars, or clinician availability with FullCalendar, Cal.com-style flows, or custom slot pickers. Healthcare, services, and B2B meeting tools.
Why testing calendar scheduling matters
Scheduling errors destroy trust immediately:
- Double booking — two clients same slot; HIPAA and SLA incidents in healthcare.
- Timezone errors — patient arrives wrong hour; pair with datetime guide.
- Buffer violations — 15-minute back-to-back when policy requires 30; provider burnout and billing issues.
- Capacity limits — group class overbooked; refunds and reputational harm.
- Cancellation windows — late cancel allowed when policy forbids; revenue leakage.
Probe booking exclusivity, UTC instants, and capacity counters—calendar cell color is not proof.
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| Double-book race | Two tabs same slot | Both confirm | Parallel requests + probe |
| Timezone display | User vs resource TZ | Wrong label | Probe UTC + display |
| DST day | Missing slot | Click fail | clock boundary spec |
| Buffer time | Adjacent slot hidden | Policy bypass | Probe gap minutes |
| Resource pool | Any room vs specific | Wrong asset | Probe resource_id |
| Recurring series | Exception dates | Orphan instance | Probe series id |
| Waitlist | Slot freed | Promotion | Inject cancel + probe |
| Blackout dates | Holiday | Slot shown | Negative click |
| Min notice | Book 1h ahead | Should block | clock + probe 422 |
| Group capacity | 9/10 seats | Overbook | Probe seats_remaining |
| External sync | Google Calendar lag | Stale free/busy | Mock sync probe |
| Cancel/reschedule | Old slot freed | Double capacity | Probe slot counts |
Seed slot inventory
await request.post('/api/test/seed-calendar', {
data: {
runId,
providerId: 'dr-1',
slots: [
{ startUtc: '2024-06-15T14:00:00Z', capacity: 1 },
{ startUtc: '2024-06-15T15:00:00Z', capacity: 1 },
],
},
});
Happy path booking
await page.goto('/book/dr-1');
await page.getByRole('button', { name: /Jun 15.*2:00 PM/i }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
const booking = await request.get(`/api/test/bookings?runId=${runId}`).then(r => r.json());
expect(booking).toHaveLength(1);
expect(booking[0].startUtc).toBe('2024-06-15T14:00:00Z');
Double-booking negative (critical)
const slot = '2024-06-15T14:00:00Z';
const [r1, r2] = await Promise.all([
request.post('/api/test/book', { data: { runId, slot, client: 'a' } }),
request.post('/api/test/book', { data: { runId, slot, client: 'b' } }),
]);
const statuses = [r1.status(), r2.status()];
expect(statuses.filter(s => s === 201)).toHaveLength(1);
expect(statuses.filter(s => s === 409)).toHaveLength(1);
const bookings = await request.get(`/api/test/bookings?runId=${runId}&slot=${slot}`).then(r => r.json());
expect(bookings).toHaveLength(1);
Run UI parallel spec too: two Playwright contexts racing Confirm.
Requirement slices to cover
timezone— booker and resourceslot_type— initial, follow-up, telehealth, group
Healthcare portals: see healthcare guide for PHI-safe synthetic patients.
CI checklist
- Unique calendar per runId—parallel workers never share last slot
- Double-book race spec (API + optional UI)
- clock.install for min-notice and DST specs
- Probe UTC on every booking
- Cancel/reschedule frees slot—probe count
- Group capacity when product supports classes
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Shared "last slot" in CI | Parallel false 409 | Per-run inventory |
| Assert cell CSS color | Theme flake | Probe booking row |
| Skip race test | Prod double-book | Promise.all book |
| Local string time only | TZ bugs | UTC probe |
| waitFor calendar paint | Slow | click slot + probe |
| Ignore cancel window | Policy bug | clock + negative |
Example scenario
Situation: Two patients click the last available telehealth slot within 200ms.
Expected outcome: Exactly one booking; second user sees slot unavailable; no duplicate clinician event.
Why UI-only automation breaks: Second user sees error toast but probe shows two bookings—calendar sync corrupt.
- Arrange: Seed one remaining slot at 2024-06-15T14:00:00Z for runId.
- Act: Parallel UI confirm or API book race.
- Assert: Probe bookings.length=1; second response 409; slot marked unavailable in inventory probe.
TestChimp workflow: Instrument booking_attempt with slot_type and timezone; evolve telehealth slice if prod share grows.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- Date/time/timezones — DST and UTC
- Healthcare portals — patient booking
- Form validation — intake forms
- WebSockets — live calendar updates
External references
Frequently asked questions
How do I prevent double-booking in parallel tests?
Use per-run calendar seeds; run API race with Promise.all expecting one 201 and one 409; optional dual-browser UI race on last slot.
How do timezones affect slot pickers?
Probe stored UTC always; display strings vary by user timezone—use clock and seeded user TZ from datetime guide patterns.
How do I test minimum booking notice?
Install clock near slot boundary, attempt book inside forbidden window, probe 422 and UI error—no booking row created.
Group classes vs 1:1 slots?
Seed capacity>1, book until full, assert next book fails with probe seats_remaining=0.
Cancel and reschedule?
Book, cancel via UI, probe slot available again; reschedule moves booking id without duplicate occupancy.
Which slot_types need TrueCoverage priority?
Compare prod booking_attempt distributions—telehealth and weekend slots often missed in generic weekday office-hour tests.
Healthcare-specific concerns?
Synthetic patients only; never real PHI in URLs or probes; pair with HIPAA-aware fixtures in healthcare vertical guide.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.