From a948e7fc260bdf6faa13d6489b004aa587cc0bf1 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 20:49:57 +0100 Subject: [PATCH] test: check the calendar picker and the kit checkbox against the app's one menu, which draws a tick instead of a box and closes on page scroll after it opens The booking calendar picker is now the app's context menu: its header is the menu's header row, it lays out from its measured width and starts listening for scroll a tick after it opens. Menu check items show a tick instead of a drawn box, so the kit checkbox copies the Files select box only. --- .../__tests__/use-booking-calendar.test.tsx | 22 ++++++++++++++++--- lib/__tests__/plugin-ui-kit.test.tsx | 4 +++- lib/plugin-sandbox/ui-kit/components.tsx | 2 +- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/components/email/structured-mail/__tests__/use-booking-calendar.test.tsx b/components/email/structured-mail/__tests__/use-booking-calendar.test.tsx index 10977d55f..5953d6462 100644 --- a/components/email/structured-mail/__tests__/use-booking-calendar.test.tsx +++ b/components/email/structured-mail/__tests__/use-booking-calendar.test.tsx @@ -349,7 +349,12 @@ describe('BookingCalendarActions', () => { expect(document.activeElement).toBe(items[2]); fireEvent.keyDown(menu, { key: 'Home' }); expect(document.activeElement).toBe(items[0]); - expect(menu.querySelector('div')?.getAttribute('role')).toBe('presentation'); + // The heading is the app menu's header row: not an item, so the keys skip it. + const header = menu.querySelector('[data-context-menu-header]'); + expect(header?.textContent).toBe('select_calendar'); + expect(header?.closest('[role="menuitem"]')).toBeNull(); + expect(header?.hasAttribute('tabindex')).toBe(false); + expect(items.map((item) => item.textContent)).toEqual(['Personal', 'Work', 'Travel']); }); it('closes the picker on Escape and gives focus back to its button', async () => { @@ -361,9 +366,12 @@ describe('BookingCalendarActions', () => { it('stays open while its own list scrolls, and closes when the page scrolls', async () => { const { menu } = await openPicker(); - fireEvent.scroll(menu); + // The menu starts listening a tick after it opens, so the click that opened it doesn't close it. + await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); + fireEvent.scroll(menu.firstElementChild ?? menu); expect(screen.queryByRole('menu')).not.toBeNull(); - fireEvent.scroll(window); + // A page scroll is fired at the document. + fireEvent.scroll(document); expect(screen.queryByRole('menu')).toBeNull(); }); @@ -378,6 +386,13 @@ describe('BookingCalendarActions', () => { useCalendarStore.setState({ calendars: [cal('Personal', { isDefault: true }), cal('Work')] }); const width = window.innerWidth; Object.defineProperty(window, 'innerWidth', { configurable: true, value: 390 }); + // jsdom lays nothing out: the menu measures as 208 px wide, as it is on a phone. + const measure = vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) { + const isMenu = this.getAttribute('role') === 'menu'; + const w = isMenu ? 208 : 0; + const h = isMenu ? 120 : 0; + return { left: 0, right: w, top: 0, bottom: h, width: w, height: h, x: 0, y: 0, toJSON: () => ({}) }; + }); try { render(); const button = await screen.findByRole('button', { name: /add_to_calendar/ }); @@ -388,6 +403,7 @@ describe('BookingCalendarActions', () => { expect(left).toBeGreaterThanOrEqual(8); expect(left + 208).toBeLessThanOrEqual(390 - 8); } finally { + measure.mockRestore(); Object.defineProperty(window, 'innerWidth', { configurable: true, value: width }); } }); diff --git a/lib/__tests__/plugin-ui-kit.test.tsx b/lib/__tests__/plugin-ui-kit.test.tsx index cdff99ff2..3d45119c6 100644 --- a/lib/__tests__/plugin-ui-kit.test.tsx +++ b/lib/__tests__/plugin-ui-kit.test.tsx @@ -65,7 +65,9 @@ describe('plugin UI kit components', () => { ['components/settings/settings-section.tsx', 'bg-muted-foreground ltr:translate-x-1 rtl:-translate-x-1'], ['components/settings/settings-section.tsx', 'flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4 py-3 border-b border-border last:border-0'], ['components/settings/settings-section.tsx', 'px-3 py-1.5 text-xs rounded-md transition-colors duration-150'], - ['components/email/composer-toolbar-menu.tsx', 'mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border'], + ['components/files/file-views.tsx', 'w-4 h-4 rounded border flex items-center justify-center shrink-0'], + ['components/files/file-views.tsx', 'bg-primary border-primary text-primary-foreground'], + ['components/files/file-views.tsx', 'border-muted-foreground/50 bg-background'], ['components/calendar/ical-import-modal.tsx', 'flex items-center gap-1.5 px-4 py-2 text-sm font-medium border-b-2 transition-colors'], ['components/ui/empty-state.tsx', 'w-14 h-14 rounded-full bg-muted flex items-center justify-center mb-4'], ['components/files/file-views.tsx', 'border-b border-border/60'], diff --git a/lib/plugin-sandbox/ui-kit/components.tsx b/lib/plugin-sandbox/ui-kit/components.tsx index 328777195..04e97f00d 100644 --- a/lib/plugin-sandbox/ui-kit/components.tsx +++ b/lib/plugin-sandbox/ui-kit/components.tsx @@ -669,7 +669,7 @@ export interface KitCheckboxProps { id?: string; } -/** The composer's check items and the Files app's select box: a drawn box, not the browser's. */ +/** The Files app's select box: a drawn box, not the browser's. */ export function Checkbox({ checked, onChange, label, hint, disabled = false, id, ariaLabel }: KitCheckboxProps) { const autoId = React.useId(); const boxId = id ?? autoId;