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('');
});
});