GraphSidebar.test.tsx3.8 KBView on GitHub
/**
 * The canvas's left rail.
 *
 * Mounted directly rather than through the canvas: what is being asserted is which glyphs the
 * rail offers for a given set of callbacks and what each one is CALLED, and a React Flow
 * harness would add a provider and a graph read around that.
 *
 * The recurring assertion is `getByRole('button', { name })` — an icon-only control whose
 * accessible name is missing is invisible to a screen reader and unnameable by a test, which
 * is the same failure wearing two hats.
 */

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { GraphSchema } from '@zero/server/graph';

import { GraphSidebar } from '@/modules/graph/GraphSidebar';

const SCHEMA: GraphSchema = {
  version: 1,
  visibility: 'team',
  nodeSchema: {
    fields: [
      { key=[redacted], label: 'Stance', type: 'select', options: ['supporter', 'skeptic'] },
    ],
  },
  edgeKinds: [{ key=[redacted], label: 'Reports to' }],
  view: { colorField: 'stance' },
};

describe('GraphSidebar', () => {
  it('offers the two actions as named, icon-only buttons', async () => {
    const onAddNode = jest.fn();
    const onAutoFormat = jest.fn();
    render(<GraphSidebar schema={SCHEMA} onAddNode={onAddNode} onAutoFormat={onAutoFormat} />);

    const add = screen.getByRole('button', { name: 'Add node' });
    const format = screen.getByRole('button', { name: 'Auto-format' });

    // Icon-only: the label is the accessible name and the tooltip, never text on the canvas.
    expect(add).toHaveTextContent('');
    expect(format).toHaveTextContent('');

    await userEvent.click(add);
    expect(onAddNode).toHaveBeenCalledTimes(1);
    await userEvent.click(format);
    expect(onAutoFormat).toHaveBeenCalledTimes(1);
  });

  it('is one vertical toolbar, not three loose buttons', () => {
    render(<GraphSidebar schema={SCHEMA} onAddNode={jest.fn()} />);
    const rail = screen.getByRole('toolbar', { name: 'Graph actions' });
    expect(rail).toHaveAttribute('aria-orientation', 'vertical');
  });

  it('drops an action it was given no handler for', () => {
    render(<GraphSidebar schema={SCHEMA} onAddNode={jest.fn()} />);
    expect(screen.queryByRole('button', { name: 'Auto-format' })).toBeNull();
    expect(screen.queryByRole('button', { name: 'Schema' })).toBeNull();
  });

  /**
   * The read-only case. A raised strip on the canvas that holds nothing is a control that does
   * nothing, which is worse than no control — so the rail removes itself entirely.
   */
  it('renders nothing at all when it can offer nothing', () => {
    const { container } = render(<GraphSidebar schema={SCHEMA} />);
    expect(container).toBeEmptyDOMElement();
  });

  /**
   * The two schema writes travel together. Half an editor — one that can repoint the colour
   * field but not tick a field onto the node face — is a form with a dead half, and nothing on
   * screen would say which half.
   */
  it('hides Schema unless it can perform both schema writes', () => {
    const { rerender } = render(<GraphSidebar schema={SCHEMA} onSetView={jest.fn()} />);
    expect(screen.queryByRole('button', { name: 'Schema' })).toBeNull();

    rerender(<GraphSidebar schema={SCHEMA} onSetFields={jest.fn()} />);
    expect(screen.queryByRole('button', { name: 'Schema' })).toBeNull();

    rerender(<GraphSidebar schema={SCHEMA} onSetView={jest.fn()} onSetFields={jest.fn()} />);
    expect(screen.getByRole('button', { name: 'Schema' })).toBeInTheDocument();
  });

  it('opens the schema editor from the Schema glyph', async () => {
    render(<GraphSidebar schema={SCHEMA} onSetView={jest.fn()} onSetFields={jest.fn()} />);

    await userEvent.click(screen.getByRole('button', { name: 'Schema' }));

    expect(await screen.findByText('Background')).toBeInTheDocument();
    expect(screen.getByText('On the node face')).toBeInTheDocument();
  });
});