calendarCanvasKeepsChat.test.tsx7.0 KBView on GitHub import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router';
import React from 'react';
/**
* ⇧Q is the /calendar PAGE dropped over the route you were on: the grid takes the MAIN column and
* the column beside it becomes the calendar's own rail — not one overlay painted across both.
*
* The overlay used to span the whole shell, which buried the chat and, with it, the event editor
* the chat column swaps in for a draft event: you could open the calendar from anywhere and then
* had no way to finish creating an event (adding a Google Meet especially) without closing it.
* Then it kept the chat but left the ROUTE's surface in it, so /home answered ⇧Q by squeezing the
* home hero into a sidebar beside the grid.
*/
jest.mock('@/components/layout/ChatColumn', () => ({
ChatColumn: () => {
const R = require('react');
return R.createElement('div', { 'data-testid': 'chat' }, 'CHAT');
},
}));
jest.mock('@/modules/calendar/components/CalendarSidebar', () => ({
CalendarSidebar: () => {
const R = require('react');
return R.createElement('div', { 'data-testid': 'event-editor' }, 'EVENT EDITOR');
},
}));
jest.mock('@/components/ui/GlobalCanvas', () => ({
// Gated like the real one, so "is a canvas on screen" means the same thing here as in the app.
GlobalCanvas: () => {
const R = require('react');
const { useCedarStore } = require('@/store/CedarStore');
const open = useCedarStore((s: { isGlobalCanvasOpen: boolean }) => s.isGlobalCanvasOpen);
return open ? R.createElement('div', { 'data-testid': 'global-canvas' }, 'CALENDAR') : null;
},
}));
jest.mock('@/modules/ux/layout/useRouteChatThread', () => ({
useRouteChatThread: () => {},
}));
import { selectCalendarRail } from '@/modules/ux/layout/selectCalendarRail';
import { CALENDAR_RAIL_WIDTH } from '@/components/layout/widthRegistry';
import { DEFAULT_THREAD_ID } from '@/store/messages/MessageTypes';
import { AppShell } from '@/components/layout/AppShell';
import { useCedarStore } from '@/store/CedarStore';
/** /home at rest — the chat-dominant arrangement, where the chat is the only column. */
function setHomeAtRest(over: Partial<Record<string, unknown>> = {}) {
act(() => {
useCedarStore.setState((s) => ({
...s,
navigation: { page: 'home' },
mainThreadId: DEFAULT_THREAD_ID,
activeThreadId: DEFAULT_THREAD_ID,
selectedThreadId: null,
threadMap: {
[DEFAULT_THREAD_ID]: {
id: DEFAULT_THREAD_ID,
lastLoaded: new Date().toISOString(),
messages: [],
selectedArtifact: null,
},
},
isGlobalCanvasOpen: false,
activeCanvasId: null,
draftCalendarEvent: null,
...over,
}));
});
}
const renderShell = () =>
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={['/home']}>
<AppShell>
<div data-testid="route-content">route-content</div>
</AppShell>
</MemoryRouter>
</QueryClientProvider>,
);
/**
* The tree under test reaches `useCalendarCanvasOverlay`, which holds a query client so it
* can refetch the week on the way into the calendar overlay. The app always has the provider
* above these components; without one here the hook throws before anything renders.
*
* `retry: false` so a stray query fails fast instead of holding the test open.
*/
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
describe('the calendar canvas and the chat', () => {
it('keeps the chat on screen beside the calendar', () => {
setHomeAtRest();
renderShell();
expect(screen.queryByTestId('global-canvas')).toBeNull();
act(() => {
useCedarStore.setState((s) => ({
...s,
isGlobalCanvasOpen: true,
activeCanvasId: 'calendar',
}));
});
expect(screen.getByTestId('global-canvas')).toBeInTheDocument();
expect(screen.getByTestId('chat')).toBeInTheDocument();
});
it('leaves the chat alone while the grid is up — the draft is composed on its own block', () => {
setHomeAtRest({ isGlobalCanvasOpen: true, activeCanvasId: 'calendar' });
renderShell();
act(() => {
useCedarStore.setState((s) => ({
...s,
draftCalendarEvent: { summary: 'Intro call', start: {}, end: {} },
}));
});
// With the grid on screen the draft has a block of its own, so the composer is pinned to
// it (EventComposerPopover) and the chat column is not swapped out to hold a form about
// something already on screen.
expect(screen.queryByTestId('event-editor')).not.toBeInTheDocument();
expect(screen.getByTestId('chat')).toBeInTheDocument();
expect(screen.getByTestId('global-canvas')).toBeInTheDocument();
});
it('swaps in the editor panel for a draft raised with no grid to pin to', () => {
// A follow-up scheduled from a thread, or the chat's proposed-event card: there is no block
// on screen to anchor a popover to, so the chat column still carries the full editor.
setHomeAtRest({ isGlobalCanvasOpen: false, activeCanvasId: null });
renderShell();
act(() => {
useCedarStore.setState((s) => ({
...s,
draftCalendarEvent: { summary: 'Intro call', start: {}, end: {} },
}));
});
expect(screen.getByTestId('event-editor')).toBeInTheDocument();
});
it('does not drag the agent page up behind the calendar', () => {
// /home renders nothing into the context column at rest, so borrowing that column for the
// calendar must not start mounting the route's content there.
setHomeAtRest({ isGlobalCanvasOpen: true, activeCanvasId: 'calendar' });
renderShell();
expect(screen.queryByTestId('route-content')).toBeNull();
});
it('hands the chat column to the calendar rail while the grid is down', () => {
// ⇧Q is the /calendar PAGE dropped over the route you were on, so the column beside the grid
// is the calendar's own rail (month · "Meet with" · calendars) pinned to the month grid's
// width — not the home hero squeezed into a sidebar, which is what it used to be.
setHomeAtRest();
renderShell();
expect(selectCalendarRail(useCedarStore.getState())).toBe(false);
act(() => {
useCedarStore.setState((s) => ({
...s,
isGlobalCanvasOpen: true,
activeCanvasId: 'calendar',
}));
});
expect(selectCalendarRail(useCedarStore.getState())).toBe(true);
expect(screen.getByTestId('chat').closest('[style*="width"]')).toHaveStyle({
width: `${CALENDAR_RAIL_WIDTH}px`,
});
});
it('leaves every other canvas covering the whole shell', () => {
setHomeAtRest({ isGlobalCanvasOpen: true, activeCanvasId: 'canvas_crm_filter' });
renderShell();
// Still exactly one canvas mount, and the chat still renders behind it (the overlay covers
// it visually — that is the untouched behaviour for non-calendar canvases).
expect(screen.getAllByTestId('global-canvas')).toHaveLength(1);
});
});