CardFieldList.test.tsx6.7 KBView on GitHub
/**
 * A card's typed field list — the surface the whole board type exists to produce.
 *
 * Mounted directly rather than through `CardDocumentView`: the point of these components is
 * WHAT THEY RENDER for a given (schema, values) pair, and a view harness would add a tRPC
 * query and a Y.js provider around that one assertion.
 *
 * The openness cases here are the FOURTH of the four ways to silently close an open schema —
 * a UI that hides unknown keys lets a human conclude the agent wrote nothing, then "fix" the
 * schema without them. The other three are on the server and have their own tests.
 */

import { fireEvent, render, screen } from '@testing-library/react';
import type { BoardSchema } from '@zero/server/board';

import { CardFieldList } from '@/modules/documents/board/CardFieldList';
import { CardFieldDisplay, toFieldValue } from '@/modules/documents/board/CardFieldValue';

function schemaWith(fields: BoardSchema['fields']): BoardSchema {
  return { version: 1, visibility: 'team', defaultLane: 'intake', lanes: [], fields };
}

describe('CardFieldList', () => {
  it("renders every field the BOARD declares, even ones this card has no value for", () => {
    // The board is a CONTRACT about what a ticket carries, not a description of what one
    // happens to hold — so an unfilled field is an empty row, not a missing one.
    render(
      <CardFieldList
        schema={schemaWith([
          { key=[redacted], label: 'Severity', type: 'select', options: ['p0', 'p1'] },
          { key=[redacted], label: 'Customer', type: 'text' },
        ])}
        fields={{ severity: 'p1' }}
      />,
    );

    expect(screen.getByText('Severity')).toBeInTheDocument();
    expect(screen.getByText('Customer')).toBeInTheDocument();
    expect(screen.getByText('p1')).toBeInTheDocument();
    expect(screen.getByText('Empty')).toBeInTheDocument();
  });

  it('SHOWS a value whose key the board does not declare, under its own heading', () => {
    render(
      <CardFieldList
        schema={schemaWith([{ key=[redacted], label: 'Severity', type: 'text' }])}
        fields={{ severity: 'p2', customer_impact: 'high' }}
      />,
    );

    expect(screen.getByText('Not declared by this board')).toBeInTheDocument();
    expect(screen.getByText('customer_impact')).toBeInTheDocument();
    expect(screen.getByText('high')).toBeInTheDocument();
  });

  it('names the declared type when Cedar does not know it, rather than hiding the field', () => {
    // An agent that invented `type: 'sentiment'` gets a working text field today. Saying so is
    // what stops someone "fixing" the field by deleting it.
    render(
      <CardFieldList schema={schemaWith([{ key=[redacted], label: 'Mood', type: 'sentiment' }])} fields={{}} />,
    );
    expect(screen.getByText('sentiment · shown as text')).toBeInTheDocument();
  });

  it('shows a known non-text type plainly, with no "shown as" caveat', () => {
    render(
      <CardFieldList schema={schemaWith([{ key: 'due', label: 'Due', type: 'date' }])} fields={{}} />,
    );
    expect(screen.getByText('date')).toBeInTheDocument();
    expect(screen.queryByText(/shown as text/)).not.toBeInTheDocument();
  });

  it('is read-only with no onChange — rows render but do not open', () => {
    render(
      <CardFieldList schema={schemaWith([{ key: 'a', label: 'A', type: 'text' }])} fields={{ a: 'v' }} />,
    );
    expect(screen.getByRole('button', { name: /v/ })).toBeDisabled();
  });

  it('opens one field at a time and commits its new value', () => {
    const onChange = jest.fn();
    render(
      <CardFieldList
        schema={schemaWith([{ key=[redacted], label: 'Customer', type: 'text' }])}
        fields={{ customer: 'Hey Telo' }}
        onChange={onChange}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: /Hey Telo/ }));
    const input = screen.getByLabelText('Customer');
    fireEvent.change(input, { target: { value: 'Acme' } });
    fireEvent.keyDown(input, { key=[redacted] });

    expect(onChange).toHaveBeenCalledWith('customer', 'Acme');
  });

  it('Escape closes the editor without writing', () => {
    const onChange = jest.fn();
    render(
      <CardFieldList
        schema={schemaWith([{ key=[redacted], label: 'Customer', type: 'text' }])}
        fields={{ customer: 'Hey Telo' }}
        onChange={onChange}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: /Hey Telo/ }));
    fireEvent.keyDown(screen.getByLabelText('Customer'), { key=[redacted] });

    expect(onChange).not.toHaveBeenCalled();
  });

  it('says how to start when the board declares nothing, rather than showing a blank', () => {
    render(<CardFieldList schema={schemaWith([])} fields={{}} />);
    expect(screen.getByText(/declares no fields yet/)).toBeInTheDocument();
    // …and points at adoption, which is the thing a reader would not guess.
    expect(screen.getByText(/adopted into the board/)).toBeInTheDocument();
  });
});

describe('CardFieldDisplay — typed rendering', () => {
  it('renders a select as a coloured pill, a checkbox as a box, a URL as a link', () => {
    const { rerender } = render(
      <CardFieldDisplay
        field={{ key: 's', label: 'S', type: 'select', options: ['p0'] }}
        value="p0"
      />,
    );
    expect(screen.getByText('p0')).toBeInTheDocument();

    rerender(<CardFieldDisplay field={{ key: 'c', label: 'C', type: 'checkbox' }} value="true" />);
    expect(screen.getByText('Yes')).toBeInTheDocument();

    rerender(<CardFieldDisplay field={{ key: 'u', label: 'U', type: 'url' }} value="https://x.test" />);
    expect(screen.getByRole('link')).toHaveAttribute('href', 'https://x.test');
  });

  it('renders a multi_select as one pill per value', () => {
    render(
      <CardFieldDisplay
        field={{ key: 'a', label: 'A', type: 'multi_select', options: ['x', 'y'] }}
        value="x, y"
      />,
    );
    expect(screen.getByText('x')).toBeInTheDocument();
    expect(screen.getByText('y')).toBeInTheDocument();
  });

  it('falls back to text for a type the vocabulary does not know', () => {
    render(<CardFieldDisplay field={{ key: 'm', label: 'M', type: 'sentiment' }} value="frustrated" />);
    expect(screen.getByText('frustrated')).toBeInTheDocument();
  });
});

describe('toFieldValue', () => {
  it('renders a non-string value legibly rather than as [object Object]', () => {
    // A field value is whatever an agent put there. An object under a field key is a surprise,
    // not an error — and it must stay readable.
    expect(toFieldValue({ nested: true })).toBe('{"nested":true}');
    expect(toFieldValue(42)).toBe('42');
    expect(toFieldValue(false)).toBe('false');
    expect(toFieldValue(null)).toBe('');
    expect(toFieldValue(undefined)).toBe('');
  });
});