From 919e1cc2c0e3b35ee9e47c8bf9fd311e727e138e Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Sat, 1 Aug 2026 17:52:11 -0600 Subject: [PATCH 1/2] fix(web): stabilize up next card --- .../Sidebar/UpNextCard/UpNextCard.test.tsx | 71 +++++++++++++++++-- .../Sidebar/UpNextCard/UpNextCard.tsx | 45 ++++++------ 2 files changed, 90 insertions(+), 26 deletions(-) diff --git a/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.test.tsx b/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.test.tsx index b04e522321..d6b72a4da0 100644 --- a/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.test.tsx +++ b/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.test.tsx @@ -1,8 +1,17 @@ import userEvent from "@testing-library/user-event"; -import { EventIdSchema } from "@core/types/domain-primitives"; +import { + CalendarIdSchema, + EventIdSchema, + TimeZoneSchema, +} from "@core/types/domain-primitives"; +import { + type Calendar, + getCalendarCapabilities, +} from "@core/types/calendar.contracts"; import { type Event, EventScheduleSchema } from "@core/types/event.contracts"; import dayjs from "@core/util/date/dayjs"; import { + act, cleanup, fireEvent, render, @@ -10,6 +19,9 @@ import { waitFor, } from "@web/__tests__/__mocks__/mock.render"; import { createMockEvent } from "@web/__tests__/utils/factories/event.factory"; +import { createCompassQueryClient } from "@web/api/query-client"; +import { calendarQueryKeys } from "@web/calendars/calendar.query"; +import { setCalendarVisibility } from "@web/calendars/calendar-visibility.store"; import { draftActions, useDraftStore } from "@web/events/stores/draft.store"; import { formatStartsIn, UpNextCard } from "./UpNextCard"; import { useUpNextEventShortcut } from "./useUpNextEvent"; @@ -18,6 +30,21 @@ import "@testing-library/jest-dom"; const SOON_EVENT_ID = "aaaaaaaaaaaaaaaaaaaaaaaa"; const LATER_EVENT_ID = "bbbbbbbbbbbbbbbbbbbbbbbb"; +const CALENDAR_ID = CalendarIdSchema.parse("cccccccccccccccccccccccc"); +const CALENDAR: Calendar = { + id: CALENDAR_ID, + name: "Work", + description: "", + timeZone: TimeZoneSchema.parse("America/Denver"), + foregroundColor: "#000000", + backgroundColor: "#3b82f6", + provider: "google", + access: "owner", + capabilities: getCalendarCapabilities("owner"), + isPrimary: false, + isVisible: true, + isActive: true, +}; // Events are built relative to the real clock because the card's whole job is // comparing today's events against "now". @@ -84,7 +111,7 @@ describe("UpNextCard", () => { expect(screen.getByText("N")).toBeInTheDocument(); }); - it("renders nothing when today has no upcoming timed events", () => { + it("keeps its slot with an all-clear state when today has no upcoming timed events", () => { render(, { events: [ // Already underway, so nothing is "up next". @@ -101,12 +128,44 @@ describe("UpNextCard", () => { ], }); - expect(screen.queryByRole("region", { name: "Up next" })).toBeNull(); - expect( - screen.queryByText("Nothing scheduled — press C to add an event."), - ).toBeNull(); + expect(screen.getByRole("region", { name: "Up next" })).toBeInTheDocument(); + expect(screen.getByText("All clear")).toBeInTheDocument(); expect(screen.queryByText("Past Event")).toBeNull(); expect(screen.queryByText("All Day Event")).toBeNull(); + expect(screen.queryByRole("button", { name: /up next:/i })).toBeNull(); + }); + + it("keeps its card while hiding the calendar with the only upcoming event", async () => { + const queryClient = createCompassQueryClient(); + queryClient.setQueryData(calendarQueryKeys.all, [CALENDAR]); + + render(, { + events: [ + createMockEvent({ + id: EventIdSchema.parse(SOON_EVENT_ID), + calendarId: CALENDAR_ID, + content: { kind: "details", title: "Soon Event", description: "" }, + schedule: EventScheduleSchema.parse({ + kind: "timed", + start: dayjs().add(30, "minute").format(), + end: dayjs().add(60, "minute").format(), + timeZone: "UTC", + }), + }), + ], + queryClient, + }); + + expect(screen.getByRole("button", { name: /up next: soon event/i })).toBeInTheDocument(); + + act(() => { + expect(setCalendarVisibility(CALENDAR_ID, false)).toBe(true); + }); + + await waitFor(() => { + expect(screen.getByRole("region", { name: "Up next" })).toBeInTheDocument(); + expect(screen.getByText("All clear")).toBeInTheDocument(); + }); }); it("opens the event's details in the sidebar when clicked", async () => { diff --git a/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.tsx b/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.tsx index 47771e198d..9b59f610e6 100644 --- a/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.tsx +++ b/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.tsx @@ -29,29 +29,34 @@ export function formatStartsIn(start: Dayjs, now: Dayjs): string { */ export const UpNextCard: FC = () => { const { now, openEventDetails, upNext } = useUpNextEvent(); - - if (!upNext) { - return null; - } - - const countdown = formatStartsIn(dayjs(upNext.startDate), now); + const countdown = upNext + ? formatStartsIn(dayjs(upNext.startDate), now) + : undefined; return (
- + {upNext ? ( + + ) : ( +

+ All clear +

+ )}
); }; From e05331ca23dbcdad78929524cb4b77ad24fe9df0 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Sat, 1 Aug 2026 17:56:49 -0600 Subject: [PATCH 2/2] chore(web): format up next card --- .../Sidebar/UpNextCard/UpNextCard.test.tsx | 16 ++++++++++------ .../components/Sidebar/UpNextCard/UpNextCard.tsx | 4 +--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.test.tsx b/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.test.tsx index d6b72a4da0..a5d83c0ca6 100644 --- a/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.test.tsx +++ b/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.test.tsx @@ -1,13 +1,13 @@ import userEvent from "@testing-library/user-event"; +import { + type Calendar, + getCalendarCapabilities, +} from "@core/types/calendar.contracts"; import { CalendarIdSchema, EventIdSchema, TimeZoneSchema, } from "@core/types/domain-primitives"; -import { - type Calendar, - getCalendarCapabilities, -} from "@core/types/calendar.contracts"; import { type Event, EventScheduleSchema } from "@core/types/event.contracts"; import dayjs from "@core/util/date/dayjs"; import { @@ -156,14 +156,18 @@ describe("UpNextCard", () => { queryClient, }); - expect(screen.getByRole("button", { name: /up next: soon event/i })).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /up next: soon event/i }), + ).toBeInTheDocument(); act(() => { expect(setCalendarVisibility(CALENDAR_ID, false)).toBe(true); }); await waitFor(() => { - expect(screen.getByRole("region", { name: "Up next" })).toBeInTheDocument(); + expect( + screen.getByRole("region", { name: "Up next" }), + ).toBeInTheDocument(); expect(screen.getByText("All clear")).toBeInTheDocument(); }); }); diff --git a/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.tsx b/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.tsx index 9b59f610e6..bd1971e4d7 100644 --- a/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.tsx +++ b/packages/web/src/components/Sidebar/UpNextCard/UpNextCard.tsx @@ -42,9 +42,7 @@ export const UpNextCard: FC = () => { onClick={() => openEventDetails("gridClick")} type="button" > - - {countdown} - + {countdown} N