From 81ec0c91844ebdc559016f15500b1a2c7fe5623d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 23:51:29 +0000 Subject: [PATCH 1/2] fix(web): teach the clicked time's shortcut on empty grid clicks Empty timed-grid clicks hit the hour-line overlay, so the hint never resolved a slot and fell back to "keyboard only". Detect those clicks and show the matching HHMM digits with a short create explanation. Co-authored-by: Tyler Dane --- docs/acceptance/shortcuts.md | 2 + docs/frontend/contextual-pointer-guidance.md | 7 +- docs/frontend/frontend-runtime-flow.md | 4 +- e2e/timed/mouse-inert.spec.ts | 14 +++ .../PointerHint/PointerHint.test.tsx | 37 +++++- .../components/PointerHint/PointerHint.tsx | 2 +- .../keyboard-only/pointer-action.test.ts | 104 +++++++++++++++-- .../shortcuts/keyboard-only/pointer-action.ts | 109 ++++++++++++------ 8 files changed, 233 insertions(+), 46 deletions(-) diff --git a/docs/acceptance/shortcuts.md b/docs/acceptance/shortcuts.md index 0dcc35a123..c3e91ae20e 100644 --- a/docs/acceptance/shortcuts.md +++ b/docs/acceptance/shortcuts.md @@ -347,6 +347,8 @@ Compass is the keyboard calendar: pointer clicks, right-clicks, and double-click - Clicking an event does not open it, but jump chips appear, the event is selected, and the hint identifies that event's exact token plus `Enter`; the shown sequence opens the event without an initial `H`. - Clicking either sidebar control does not toggle the sidebar; the hint says to press `]` and uses open/close language matching the current state. +- Clicking an empty timed-grid slot does not open a draft; the hint shows the matching HHMM digits (`1200`, `1830`) and typing those digits creates an event at that time. +- Clicking the all-day row teaches `Shift+C`. - Unannotated controls retain the generic keyboard-only fallback while contextual coverage is expanded. - Right-click does not open the context menu; `M` (or Shift+F10) on a focused event does. - Keyboard shortcuts and Enter/Space activation of buttons continue to work. diff --git a/docs/frontend/contextual-pointer-guidance.md b/docs/frontend/contextual-pointer-guidance.md index e0d392faba..4299f021dd 100644 --- a/docs/frontend/contextual-pointer-guidance.md +++ b/docs/frontend/contextual-pointer-guidance.md @@ -12,11 +12,14 @@ of a generic legend. Acceptance coverage lives in `composedPath()` and stores that attempt on the pointer-block store. 3. `PointerHint` renders copy for the attempt. Sidebar open/close teach `]`. Event open teaches the current jump token plus Enter, or `S` if no token is - available. + available. An empty timed-grid click teaches the matching HHMM digits + (`1200`, `1830`) so the same create can be typed. An empty all-day-row + click teaches `Shift+C`. 4. A primary event click also dispatches `compass:pointer-event-jump` so the mounted grid's event-jump owner can assign tokens and activate leaderless sequences. Right-clicks stay on the generic fallback so `M` still opens - the context menu. + the context menu. An empty-grid click parks that HHMM as a short-lived + teaching target so typing the shown digits creates at the clicked instant. Unannotated controls keep the generic keyboard-only fallback. diff --git a/docs/frontend/frontend-runtime-flow.md b/docs/frontend/frontend-runtime-flow.md index fe9bbe7c53..c5f36b24ca 100644 --- a/docs/frontend/frontend-runtime-flow.md +++ b/docs/frontend/frontend-runtime-flow.md @@ -97,7 +97,9 @@ Pointer suppression (always on, mounted from `RootShell`): and hover remain - keyboard-activation clicks (Enter/Space on a native button), keyboard contextmenu (Shift+F10), and synthetic `.click()` calls pass through -- blocked clicks pulse `PointerHint`, a transient "keyboard only" pill +- blocked clicks pulse `PointerHint`, a transient pill: known targets get + the matching shortcut (including HHMM digits for an empty timed-grid + click), and unannotated controls fall back to "keyboard only" See [Shortcuts](../acceptance/shortcuts.md) for acceptance coverage and [Feature File Map](../development/feature-file-map.md#keyboard-shortcuts) for diff --git a/e2e/timed/mouse-inert.spec.ts b/e2e/timed/mouse-inert.spec.ts index ca5ddab53a..cd62553b05 100644 --- a/e2e/timed/mouse-inert.spec.ts +++ b/e2e/timed/mouse-inert.spec.ts @@ -11,6 +11,20 @@ import { // Compass is the keyboard calendar: the mouse is permanently inert. These // tests are the behavioral contract for the always-on pointer suppression. +test("empty timed-grid clicks teach the matching HHMM shortcut", async ({ + page, +}) => { + await prepareCalendarPage(page); + + const { x, y } = await getMainGridPoint(page, { xRatio: 0.6, yRatio: 0.6 }); + await page.mouse.click(x, y); + + await expect(page.getByLabel("Title")).toHaveCount(0); + await expect(page.locator("[data-pointer-hint]")).toContainText( + /Type \d{4} to create an event at/, + ); +}); + test("mouse clicks are inert and show the keyboard-only hint", async ({ page, }) => { diff --git a/packages/web/src/components/PointerHint/PointerHint.test.tsx b/packages/web/src/components/PointerHint/PointerHint.test.tsx index 80977f3a1b..114f0b8a3b 100644 --- a/packages/web/src/components/PointerHint/PointerHint.test.tsx +++ b/packages/web/src/components/PointerHint/PointerHint.test.tsx @@ -222,7 +222,42 @@ describe("PointerHint", () => { }); expect(screen.getByRole("status")).toHaveTextContent( - "Press 1130 to create at 11:30 AM.", + "Type 1130 to create an event at 11:30 AM.", + ); + }); + + it("names an evening quarter-hour in 24-hour digits", () => { + render(); + + act(() => { + pointerBlockActions.pulseBlockedClick({ + actionId: "grid.timed", + gridDate: "2026-08-29", + gridTimeKey: "1830", + gridTimeLabel: "6:30 PM", + }); + }); + + expect(screen.getByRole("status")).toHaveTextContent( + "Type 1830 to create an event at 6:30 PM.", + ); + }); + + it("keeps the timed-grid shortcut sentence after the session reminder threshold", () => { + sessionStorage.setItem(HINT_COUNT_KEY, "3"); + render(); + + act(() => { + pointerBlockActions.pulseBlockedClick({ + actionId: "grid.timed", + gridDate: "2026-08-29", + gridTimeKey: "1200", + gridTimeLabel: "12:00 PM", + }); + }); + + expect(screen.getByRole("status")).toHaveTextContent( + "Type 1200 to create an event at 12:00 PM.", ); }); diff --git a/packages/web/src/components/PointerHint/PointerHint.tsx b/packages/web/src/components/PointerHint/PointerHint.tsx index 86e822ab88..5bfae5e52d 100644 --- a/packages/web/src/components/PointerHint/PointerHint.tsx +++ b/packages/web/src/components/PointerHint/PointerHint.tsx @@ -111,7 +111,7 @@ const pointerHintMessage = ({ if (attempt?.actionId === "grid.timed" && attempt.gridTimeKey) { return ( <> - Press {attempt.gridTimeKey} to create at{" "} + Type {attempt.gridTimeKey} to create an event at{" "} {attempt.gridTimeLabel ?? attempt.gridTimeKey}. ); diff --git a/packages/web/src/shortcuts/keyboard-only/pointer-action.test.ts b/packages/web/src/shortcuts/keyboard-only/pointer-action.test.ts index a7a23287a9..8c0a60cbe9 100644 --- a/packages/web/src/shortcuts/keyboard-only/pointer-action.test.ts +++ b/packages/web/src/shortcuts/keyboard-only/pointer-action.test.ts @@ -1,5 +1,8 @@ import dayjs from "@core/util/date/dayjs"; import { + DATA_TIMED_GRID_ROW, + ID_ALLDAY_COLUMNS, + ID_GRID_ALLDAY_ROW, ID_GRID_COLUMNS_TIMED, ID_GRID_MAIN, } from "@web/common/constants/web.constants"; @@ -13,7 +16,7 @@ import { teachingFromBlockedPointer, } from "@web/shortcuts/keyboard-only/pointer-action"; import { getEffectiveTimeZone } from "@web/timezone/effective-timezone.store"; -import { describe, expect, it } from "bun:test"; +import { afterEach, describe, expect, it } from "bun:test"; describe("resolveBlockedPointerAttempt", () => { it("uses the nearest annotated element in the composed path", () => { @@ -101,20 +104,45 @@ describe("teachingFromBlockedPointer", () => { }); describe("pointerGridIntentFromPointer", () => { - it("keeps the exact effective-zone quarter-hour selected by a timed click", () => { - const grid = document.createElement("div"); + const mounted: HTMLElement[] = []; + + const mount = (node: HTMLElement) => { + document.body.append(node); + mounted.push(node); + return node; + }; + + afterEach(() => { + for (const node of mounted) node.remove(); + mounted.length = 0; + }); + + const mountTimedGrid = () => { + const grid = mount(document.createElement("div")); grid.id = ID_GRID_MAIN; Object.defineProperty(grid, "scrollHeight", { value: 1440 }); grid.getBoundingClientRect = () => - ({ top: 0, left: 0, right: 200, bottom: 400 }) as DOMRect; + ({ top: 0, left: 0, right: 250, bottom: 400 }) as DOMRect; + const columns = document.createElement("div"); columns.id = ID_GRID_COLUMNS_TIMED; const column = document.createElement("div"); column.dataset.gridDate = "2026-08-29"; column.getBoundingClientRect = () => - ({ top: 0, left: 0, right: 200, bottom: 1440 }) as DOMRect; + ({ top: 0, left: 50, right: 250, bottom: 1440 }) as DOMRect; columns.appendChild(column); - document.body.append(grid, columns); + + const hourRows = document.createElement("div"); + const hourRow = document.createElement("div"); + hourRow.setAttribute(DATA_TIMED_GRID_ROW, "true"); + hourRows.appendChild(hourRow); + + grid.append(columns, hourRows); + return { column, columns, grid, hourRow, hourRows }; + }; + + it("keeps the exact effective-zone quarter-hour selected by a timed click", () => { + const { column, columns } = mountTimedGrid(); const intent = pointerGridIntentFromPointer( [column, columns, document], @@ -130,7 +158,67 @@ describe("pointerGridIntentFromPointer", () => { expect( dayjs(intent?.start).tz(getEffectiveTimeZone()).format("HH:mm"), ).toBe("11:30"); - grid.remove(); - columns.remove(); + }); + + it("reads the clicked quarter-hour from an empty hour-row overlay", () => { + const { grid, hourRow, hourRows } = mountTimedGrid(); + + const intent = pointerGridIntentFromPointer( + [hourRow, hourRows, grid, document], + 100, + 690, + ); + + expect(intent).toMatchObject({ + date: "2026-08-29", + kind: "timed", + timeKey: "1130", + }); + }); + + it("maps an evening empty-grid click to 24-hour digits", () => { + const { grid, hourRow, hourRows } = mountTimedGrid(); + + const intent = pointerGridIntentFromPointer( + [hourRow, hourRows, grid, document], + 100, + 1110, + ); + + expect(intent).toMatchObject({ + date: "2026-08-29", + kind: "timed", + timeKey: "1830", + }); + }); + + it("still teaches a time when the click is in the hour-label gutter", () => { + const { grid } = mountTimedGrid(); + + const intent = pointerGridIntentFromPointer([grid, document], 20, 690); + + expect(intent).toMatchObject({ + date: "2026-08-29", + kind: "timed", + timeKey: "1130", + }); + }); + + it("treats an empty all-day-row click as an all-day create", () => { + const row = mount(document.createElement("section")); + row.id = ID_GRID_ALLDAY_ROW; + const columns = document.createElement("div"); + columns.id = ID_ALLDAY_COLUMNS; + const column = document.createElement("div"); + column.dataset.gridDate = "2026-08-29"; + column.getBoundingClientRect = () => + ({ top: 0, left: 0, right: 200, bottom: 40 }) as DOMRect; + columns.appendChild(column); + row.appendChild(columns); + + expect(pointerGridIntentFromPointer([row, document], 100, 10)).toEqual({ + date: "2026-08-29", + kind: "all-day", + }); }); }); diff --git a/packages/web/src/shortcuts/keyboard-only/pointer-action.ts b/packages/web/src/shortcuts/keyboard-only/pointer-action.ts index 0922538bc3..e17efb8dd1 100644 --- a/packages/web/src/shortcuts/keyboard-only/pointer-action.ts +++ b/packages/web/src/shortcuts/keyboard-only/pointer-action.ts @@ -1,6 +1,9 @@ +import { YEAR_MONTH_DAY_FORMAT } from "@core/constants/date.constants"; import dayjs from "@core/util/date/dayjs"; import { + DATA_TIMED_GRID_ROW, ID_ALLDAY_COLUMNS, + ID_GRID_ALLDAY_ROW, ID_GRID_COLUMNS_TIMED, ID_GRID_EVENTS_ALLDAY, ID_GRID_EVENTS_TIMED, @@ -148,36 +151,57 @@ export const pointerGridIntent = (event: Event): PointerGridIntent | null => { }; }; -export const pointerGridIntentFromPointer = ( - path: EventTarget[], +const ALL_DAY_GRID_IDS = new Set([ + ID_ALLDAY_COLUMNS, + ID_GRID_EVENTS_ALLDAY, + ID_GRID_ALLDAY_ROW, +]); + +const TIMED_GRID_IDS = new Set([ + ID_GRID_COLUMNS_TIMED, + ID_GRID_EVENTS_TIMED, + ID_GRID_MAIN, +]); + +const gridKindFromElement = ( + target: HTMLElement, +): PointerGridIntent["kind"] | undefined => { + if (ALL_DAY_GRID_IDS.has(target.id)) return "all-day"; + if (TIMED_GRID_IDS.has(target.id)) return "timed"; + // Hour lines sit on top of the day columns, so an empty click hits a row + // (or #mainGrid) rather than #timedColumns. + if (target.hasAttribute(DATA_TIMED_GRID_ROW)) return "timed"; +}; + +const gridDateFromColumns = ( + kind: PointerGridIntent["kind"], clientX: number, +): string | undefined => { + const columnsRoot = document.getElementById( + kind === "all-day" ? ID_ALLDAY_COLUMNS : ID_GRID_COLUMNS_TIMED, + ); + const columns = [ + ...(columnsRoot?.querySelectorAll("[data-grid-date]") ?? []), + ]; + const underPointer = columns.find((column) => { + const rect = column.getBoundingClientRect(); + return clientX >= rect.left && clientX <= rect.right; + })?.dataset.gridDate; + if (underPointer) return underPointer; + + const todayKey = dayjs() + .tz(getEffectiveTimeZone()) + .format(YEAR_MONTH_DAY_FORMAT); + return ( + columns.find((column) => column.dataset.gridDate === todayKey)?.dataset + .gridDate ?? columns[0]?.dataset.gridDate + ); +}; + +const timedIntentAt = ( + date: string, clientY: number, ): PointerGridIntent | null => { - let date: string | undefined; - let kind: PointerGridIntent["kind"] | undefined; - for (const target of path) { - if (!(target instanceof HTMLElement)) continue; - date ??= target.dataset.gridDate; - if (target.id === ID_ALLDAY_COLUMNS) kind = "all-day"; - if (target.id === ID_GRID_COLUMNS_TIMED) kind = "timed"; - if (target.id === ID_GRID_EVENTS_ALLDAY) kind = "all-day"; - if (target.id === ID_GRID_EVENTS_TIMED) kind = "timed"; - } - if (!kind) return null; - if (!date) { - const columns = document.getElementById( - kind === "all-day" ? ID_ALLDAY_COLUMNS : ID_GRID_COLUMNS_TIMED, - ); - date = [ - ...(columns?.querySelectorAll("[data-grid-date]") ?? []), - ].find((column) => { - const rect = column.getBoundingClientRect(); - return clientX >= rect.left && clientX <= rect.right; - })?.dataset.gridDate; - } - if (!date) return null; - if (kind === "all-day") return { date, kind }; - const grid = document.getElementById(ID_GRID_MAIN); if (!grid) return null; const rect = grid.getBoundingClientRect(); @@ -189,18 +213,37 @@ export const pointerGridIntentFromPointer = ( ); const hour = Math.floor(minute / 60); const minutes = minute % 60; - const timeKey = `${String(hour).padStart(2, "0")}${String(minutes).padStart(2, "0")}`; + const hh = String(hour).padStart(2, "0"); + const mm = String(minutes).padStart(2, "0"); + const timeKey = `${hh}${mm}`; const timeLabel = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", }).format(new Date(2000, 0, 1, hour, minutes)); const start = dayjs - .tz( - `${date}T${String(hour).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`, - getEffectiveTimeZone(), - ) + .tz(`${date}T${hh}:${mm}`, getEffectiveTimeZone()) .format(); - return { date, kind, start, timeKey, timeLabel }; + return { date, kind: "timed", start, timeKey, timeLabel }; +}; + +export const pointerGridIntentFromPointer = ( + path: EventTarget[], + clientX: number, + clientY: number, +): PointerGridIntent | null => { + let date: string | undefined; + let kind: PointerGridIntent["kind"] | undefined; + for (const target of path) { + if (!(target instanceof HTMLElement)) continue; + date ??= target.dataset.gridDate; + kind ??= gridKindFromElement(target); + if (date && kind) break; + } + if (!kind) return null; + date ??= gridDateFromColumns(kind, clientX); + if (!date) return null; + if (kind === "all-day") return { date, kind }; + return timedIntentAt(date, clientY); }; export const pointerEventJumpId = (event: Event): string | undefined => { From d77df40d4de4d44e60954cbfdd6a370cb2661bc1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 1 Sep 2026 00:25:21 +0000 Subject: [PATCH 2/2] test(booking): keep same-day slot pairs before utc midnight Month-end evenings collapsed both stubbed slots onto 23:59 UTC, so the picker omitted the duplicate key and +30min crossed into the next day. Co-authored-by: Tyler Dane --- e2e/booking/booking-harness.ts | 28 ++++++++++ e2e/booking/public-booking.spec.ts | 19 +------ .../src/booking/PublicBookingPage.test.tsx | 56 ++++++++++++++++--- 3 files changed, 79 insertions(+), 24 deletions(-) diff --git a/e2e/booking/booking-harness.ts b/e2e/booking/booking-harness.ts index 4befc127cf..3636e7dd1a 100644 --- a/e2e/booking/booking-harness.ts +++ b/e2e/booking/booking-harness.ts @@ -106,6 +106,34 @@ export function buildBookableSlot(durationMinutes = 30): { return { slotStart: start.toISOString(), slotEnd: end.toISOString() }; } +/** Distinct sibling on the same UTC date. Steps backward when +gap would cross midnight. */ +export function buildSameDaySiblingSlot( + slot: { slotStart: string; slotEnd: string }, + gapMinutes = 30, + durationMinutes = 30, +): { slotStart: string; slotEnd: string } { + const currentMs = Date.parse(slot.slotStart); + const day = slot.slotStart.slice(0, 10); + const dayStart = Date.parse(`${day}T00:00:00.000Z`); + const lastStart = Date.parse(`${day}T23:59:00.000Z`); + let siblingMs = currentMs + gapMinutes * 60 * 1000; + if (siblingMs > lastStart || siblingMs === currentMs) { + siblingMs = currentMs - gapMinutes * 60 * 1000; + } + if (siblingMs < dayStart) { + siblingMs = dayStart; + } + const siblingStart = new Date(siblingMs); + siblingStart.setUTCSeconds(0, 0); + siblingStart.setUTCMilliseconds(0); + return { + slotStart: siblingStart.toISOString(), + slotEnd: new Date( + siblingStart.getTime() + durationMinutes * 60 * 1000, + ).toISOString(), + }; +} + export interface PublicBookingStubOptions { slug?: string; hostDisplayName?: string; diff --git a/e2e/booking/public-booking.spec.ts b/e2e/booking/public-booking.spec.ts index 67391ba59b..1519b6e984 100644 --- a/e2e/booking/public-booking.spec.ts +++ b/e2e/booking/public-booking.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from "@playwright/test"; import { buildBookableSlot, + buildSameDaySiblingSlot, formatSlotButtonLabel, preparePublicBookingConfirmedPage, preparePublicBookingPage, @@ -302,13 +303,7 @@ test.describe("public booking page", () => { page, }) => { const first = buildBookableSlot(); - const secondStart = new Date( - Date.parse(first.slotStart) + 30 * 60 * 1000, - ).toISOString(); - const second = { - slotStart: secondStart, - slotEnd: new Date(Date.parse(secondStart) + 30 * 60 * 1000).toISOString(), - }; + const second = buildSameDaySiblingSlot(first); const captured = await preparePublicBookingPage(page, { confirmStatus: 409, slots: [first, second], @@ -418,15 +413,7 @@ test.describe("public booking page", () => { page, }) => { const today = buildBookableSlot(); - const laterTodayStart = new Date( - Date.parse(today.slotStart) + 30 * 60 * 1000, - ).toISOString(); - const laterToday = { - slotStart: laterTodayStart, - slotEnd: new Date( - Date.parse(laterTodayStart) + 30 * 60 * 1000, - ).toISOString(), - }; + const laterToday = buildSameDaySiblingSlot(today); const otherDayStart = new Date( Date.parse(today.slotStart) + 25 * 60 * 60 * 1000, ).toISOString(); diff --git a/packages/web/src/booking/PublicBookingPage.test.tsx b/packages/web/src/booking/PublicBookingPage.test.tsx index 3a3ec8d00e..89eadc05f2 100644 --- a/packages/web/src/booking/PublicBookingPage.test.tsx +++ b/packages/web/src/booking/PublicBookingPage.test.tsx @@ -49,6 +49,26 @@ function slotAround(start: Date) { }; } +/** Distinct sibling on the same UTC date, stepping backward when +gap would leave the day. */ +function slotOnSameUtcDay( + current: { slotStart: string; slotEnd: string }, + gapMinutes: number, +) { + const currentMs = Date.parse(current.slotStart); + const day = current.slotStart.slice(0, 10); + const dayStart = Date.parse(`${day}T00:00:00.000Z`); + const lastStart = Date.parse(`${day}T23:59:00.000Z`); + const after = currentMs + gapMinutes * 60 * 1000; + if (after <= lastStart && after !== currentMs) { + return slotAround(new Date(after)); + } + const before = currentMs - gapMinutes * 60 * 1000; + if (before >= dayStart && before !== currentMs) { + return slotAround(new Date(before)); + } + return slotAround(new Date(dayStart)); +} + function utcTodayAt(now: Date, hours: number, minutes: number) { return new Date( Date.UTC( @@ -153,16 +173,13 @@ async function selectGuestTimeZone( const currentSlot = bookableSlotInCurrentMonth(); const laterCurrentSlot = (() => { const later = bookableSlotInCurrentMonth(30); - if (later.slotStart.slice(0, 10) === currentSlot.slotStart.slice(0, 10)) { + if ( + later.slotStart !== currentSlot.slotStart && + later.slotStart.slice(0, 10) === currentSlot.slotStart.slice(0, 10) + ) { return later; } - const currentMs = Date.parse(currentSlot.slotStart); - const endOfUtcDay = Date.parse( - `${currentSlot.slotStart.slice(0, 10)}T23:59:00.000Z`, - ); - return slotAround( - new Date(Math.min(currentMs + 15 * 60 * 1000, endOfUtcDay)), - ); + return slotOnSameUtcDay(currentSlot, 15); })(); const nextMonthSlot = bookableSlotInNextMonth(); const guestTimeZone = "UTC"; @@ -227,7 +244,30 @@ function reservationGetHandler( ); } +describe("slotOnSameUtcDay", () => { + it("steps forward when the gap still fits on the UTC day", () => { + const current = slotAround(new Date("2026-08-31T20:00:00.000Z")); + expect(slotOnSameUtcDay(current, 30).slotStart).toBe( + "2026-08-31T20:30:00.000Z", + ); + }); + + it("steps backward instead of collapsing onto 23:59 at month-end midnight", () => { + const current = slotAround(new Date("2026-08-31T23:59:00.000Z")); + const sibling = slotOnSameUtcDay(current, 15); + expect(sibling.slotStart).toBe("2026-08-31T23:44:00.000Z"); + expect(sibling.slotStart).not.toBe(current.slotStart); + }); +}); + describe("PublicBookingPage", () => { + it("uses two distinct same-day slots in the current UTC month", () => { + expect(laterCurrentSlot.slotStart).not.toBe(currentSlot.slotStart); + expect(laterCurrentSlot.slotStart.slice(0, 10)).toBe( + currentSlot.slotStart.slice(0, 10), + ); + }); + it("shows a generic not-found state for an unknown slug", async () => { renderBookingRoute("/book/unknown-host");