eventComposerPopoverDismiss.test.tsx4.5 KBView on GitHub
import { act, createEvent, fireEvent, render, screen } from '@testing-library/react';
import React from 'react';

/**
 * How the composer popover can, and cannot, be dismissed.
 *
 * Both assertions here guard bugs that are invisible in the source.
 *
 * Escape: this repo's `PopoverContent` calls `stopPropagation()` on Escape, and Radix listens
 * in the CAPTURE phase — so a `window` keydown listener inside the form never runs while the
 * popover is open. The draft therefore has to be dropped by the popover's own close. Wire it
 * the "obvious" way instead and Escape silently does nothing.
 *
 * Outside clicks, once something is typed: the grid underneath stays live so the draft block
 * can be dragged to another time while the form is up. If an outside click closed the composer,
 * moving the event would throw away everything typed into it.
 *
 * Outside clicks while it is still EMPTY: the opposite. A transparent backdrop makes the
 * composer modal, so the click means "not this" and nothing else — it never reaches the grid to
 * re-time a blank block the user has already given up on.
 */
jest.mock('@/modules/calendar/components/EventComposerForm', () => ({
  EventComposerForm: () => {
    const R = require('react');
    return R.createElement('div', { 'data-testid': 'composer-form' }, 'COMPOSER');
  },
}));

import { EventComposerPopover } from '@/modules/calendar/components/EventComposerPopover';
import { useCedarStore } from '@/modules/cedar-os/src/store/CedarStore';

function setDraft({ hasContent }: { hasContent: boolean }) {
  act(() => {
    useCedarStore.setState((s) => ({
      ...s,
      draftCalendarEvent: {
        id: 'new-1',
        summary: hasContent ? 'Intro call' : '',
        start: { dateTime: '2026-08-28T17:00:00.000Z' },
        end: { dateTime: '2026-08-28T17:30:00.000Z' },
      },
      // What the composer publishes as its fields are filled in — the popover reads it to
      // decide whether the calendar behind it is live or behind glass.
      composerHasContent: hasContent,
    }));
  });
}

function renderComposer() {
  return render(
    <EventComposerPopover>
      <div data-testid="draft-block">DRAFT</div>
    </EventComposerPopover>,
  );
}

describe('EventComposerPopover dismissal', () => {
  it('opens pinned to the draft block', () => {
    setDraft({ hasContent: true });
    renderComposer();
    expect(screen.getByTestId('draft-block')).toBeInTheDocument();
    expect(screen.getByTestId('composer-form')).toBeInTheDocument();
  });

  it('drops the draft on Escape', () => {
    setDraft({ hasContent: true });
    renderComposer();
    expect(useCedarStore.getState().draftCalendarEvent).not.toBeNull();

    act(() => {
      fireEvent.keyDown(document, { key=[redacted] });
    });

    expect(useCedarStore.getState().draftCalendarEvent).toBeNull();
  });

  it('keeps a started draft when the grid underneath is clicked', () => {
    setDraft({ hasContent: true });
    const { container } = renderComposer();
    // Stands in for the grid: dragging the block to a new time is an interaction OUTSIDE the
    // popover, and must not be read as "cancel".
    const outside = document.createElement('div');
    document.body.appendChild(outside);

    act(() => {
      fireEvent.pointerDown(outside);
      fireEvent.mouseDown(outside);
      fireEvent.click(outside);
    });

    expect(useCedarStore.getState().draftCalendarEvent).not.toBeNull();
    expect(container).toBeTruthy();

    document.body.removeChild(outside);
  });

  it('leaves the calendar live once something has been typed', () => {
    setDraft({ hasContent: true });
    renderComposer();

    expect(screen.queryByTestId('composer-backdrop')).not.toBeInTheDocument();
  });

  it('covers the calendar while nothing has been typed', () => {
    setDraft({ hasContent: false });
    renderComposer();

    expect(screen.getByTestId('composer-backdrop')).toBeInTheDocument();
  });

  it('drops an untouched draft on the first click outside, and swallows that click', () => {
    setDraft({ hasContent: false });
    renderComposer();

    const backdropClick = createEvent.pointerDown(screen.getByTestId('composer-backdrop'), {
      bubbles: true,
      cancelable: true,
    });

    act(() => {
      fireEvent(screen.getByTestId('composer-backdrop'), backdropClick);
    });

    expect(useCedarStore.getState().draftCalendarEvent).toBeNull();
    // Nullified, not passed on: the grid starts drag-to-create on `pointerdown`, so anything
    // that survives this event becomes a second draft where the user clicked.
    expect(backdropClick.defaultPrevented).toBe(true);
  });
});