createNodePopoverBoard.test.tsx5.0 KBView on GitHub
/**
 * "New → Board" in the file tree.
 *
 * The gap this closes: `#board` in the playbook editor was the ONLY way to make a board, so a
 * board existed if an agent-authoring surface happened to be open and not otherwise.
 *
 * The one thing worth asserting beyond "the mutation fires": a board does NOT land at the
 * cursor's folder. `createBoard` puts every board at `<scope>/boards/<slug>` because a board is
 * addressed BY NAME and `grantsBoard` is exact rather than prefix-covering. So the popover
 * creates into the tree's SCOPE and says where it went — a preview that lied about the path
 * would be worse than no preview.
 */
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';

const mockCreateBoard = jest.fn();

jest.mock('@/providers/query-provider', () => ({
  useTRPC: () => ({
    boards: {
      templates: { queryOptions: () => ({ queryKey: ['boards.templates'] }) },
      create: { mutationOptions: () => ({ mutationKey: ['boards.create'] }) },
    },
    files: {
      createFile: { mutationOptions: () => ({ mutationKey: ['files.createFile'] }) },
      createFolder: { mutationOptions: () => ({ mutationKey: ['files.createFolder'] }) },
    },
    kb: {
      createKbFromUrl: { mutationOptions: () => ({ mutationKey: ['kb.url'] }) },
      createKbFromGoogleDrive: { mutationOptions: () => ({ mutationKey: ['kb.drive'] }) },
    },
  }),
  trpcClient: {},
}));

jest.mock('@tanstack/react-query', () => ({
  useQueryClient: () => ({ invalidateQueries: jest.fn() }),
  useQuery: (opts: { queryKey=[redacted] }) =>
    opts.queryKey[0] === 'boards.templates'
      ? {
          data: {
            templates: [
              { id: 'customer-requests', name: 'Customer requests', description: 'x', emoji: '🗺️' },
              { id: 'work-items', name: 'Work items', description: 'y', emoji: '📋' },
            ],
          },
        }
      : { data: undefined },
  useMutation: (opts: { mutationKey=[redacted] }) => ({
    mutate: opts.mutationKey[0] === 'boards.create' ? mockCreateBoard : jest.fn(),
    isPending: false,
  }),
}));

jest.mock('@/modules/store', () => ({
  useCedarStore: Object.assign((sel: (s: unknown) => unknown) => sel({}), {
    getState: () => ({ setDocument: jest.fn() }),
  }),
  useAdminViewingUserId: () => null,
}));

jest.mock('@/modules/files/upload/useFileUpload', () => ({
  useFileUpload: () => ({ uploadToFolder: jest.fn() }),
}));

jest.mock('@/modules/integrations/use-google-picker', () => ({
  useGooglePicker: () => ({ openPicker: jest.fn(), isLoading: false, isAvailable: false }),
}));

import { CreateNodePopover } from '@/modules/company/components/CompanyExplorer';

function open(scope: { type: 'org' | 'user'; id: string }) {
  render(<CreateNodePopover scope={scope} parentId="some-folder-the-cursor-is-on" label="Actions" />);
  // The `⋮` opens a picker of "New …" rows; picking one is what opens the form behind it.
  fireEvent.click(screen.getByLabelText('Actions'));
  fireEvent.click(screen.getByText('New board'));
}

beforeEach(() => mockCreateBoard.mockClear());

describe('New → Board', () => {
  it('creates into the tree SCOPE with the chosen template, not into the selected folder', () => {
    open({ type: 'org', id: 'org-1' });

    fireEvent.change(screen.getByPlaceholderText('Board name'), {
      target: { value: 'Customer feedback' },
    });
    fireEvent.change(screen.getByLabelText('Start from'), {
      target: { value: 'customer-requests' },
    });
    fireEvent.click(screen.getByText('Save'));

    expect(mockCreateBoard).toHaveBeenCalledWith({
      name: 'Customer feedback',
      scope: 'org',
      template: 'customer-requests',
    });
    // `parentId` is deliberately absent: `boards.create` has no such input, and a board that
    // honoured it would have no name to grant.
    expect(mockCreateBoard.mock.calls[0][0]).not.toHaveProperty('parentId');
  });

  it('sends no template at all for an empty board', () => {
    open({ type: 'user', id: 'user-1' });
    fireEvent.change(screen.getByPlaceholderText('Board name'), { target: { value: 'Scratch' } });
    fireEvent.click(screen.getByText('Save'));

    expect(mockCreateBoard).toHaveBeenCalledWith({ name: 'Scratch', scope: 'user' });
  });

  it('says where the board will land, using the server\'s own slug rule', () => {
    open({ type: 'org', id: 'org-1' });
    expect(
      screen.getByText('Boards live in organisation/boards, not in the selected folder.'),
    ).toBeInTheDocument();

    fireEvent.change(screen.getByPlaceholderText('Board name'), {
      target: { value: 'Customer Feedback!!' },
    });
    expect(
      screen.getByText('Goes to organisation/boards/customer-feedback'),
    ).toBeInTheDocument();
  });

  it('offers no file upload on the board branch — there is nothing to upload into', () => {
    // Upload is a row in the picker, not a button under the form: once "New board" has been
    // chosen there is nothing on screen inviting you to upload into a board.
    open({ type: 'org', id: 'org-1' });
    expect(screen.queryByText('Upload files')).not.toBeInTheDocument();
  });
});