GraphNodeCard.test.tsx5.7 KBView on GitHub /**
* ONE node component, for every graph there will ever be.
*
* Mounted directly rather than through the canvas: the point of this component is WHAT IT
* RENDERS for a given (schema, node) pair, and a React Flow harness would add a provider and a
* tRPC query around that one assertion.
*/
import { render, screen } from '@testing-library/react';
import type { GraphNode, GraphSchema } from '@zero/server/graph';
import { GraphNodeCard } from '@/modules/graph/GraphNodeCard';
import type { GraphNodeData } from '@/modules/graph/graph-flow-types';
jest.mock('@xyflow/react', () => ({
Handle: () => null,
Position: { Top: 'top', Bottom: 'bottom' },
}));
function schemaWith(fields: GraphSchema['nodeSchema']['fields']): GraphSchema['nodeSchema'] {
return { fields };
}
function mount(data: Partial<GraphNodeData> & { node: GraphNode; nodeSchema: GraphSchema['nodeSchema'] }) {
const full: GraphNodeData = { title: data.node.title ?? data.node.documentId, ...data };
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- React Flow's NodeProps shape
return render(<GraphNodeCard {...({ data: full, selected: false } as any)} />);
}
const MARCUS: GraphNode = {
id: 'n1',
documentId: 'doc_marcus',
title: 'Marcus Lee',
fields: { stance: 'skeptic' },
};
describe('it renders FROM THE SCHEMA', () => {
it('draws one row per `displayOnCard` field, and nothing else', () => {
mount({
node: { ...MARCUS, fields: { stance: 'skeptic', private_note: 'not on the face' } },
nodeSchema: schemaWith([
{ key=[redacted], label: 'Stance', type: 'select', displayOnCard: true },
{ key: 'met', label: 'Met', type: 'select' },
]),
});
expect(screen.getByText('Marcus Lee')).toBeInTheDocument();
expect(screen.getByText('Stance')).toBeInTheDocument();
expect(screen.getByText('skeptic')).toBeInTheDocument();
// A field the schema declares but does not show on the face stays in the rail.
expect(screen.queryByText('Met')).not.toBeInTheDocument();
expect(screen.queryByText('not on the face')).not.toBeInTheDocument();
});
it('omits a shown field the node has no value for, rather than drawing an empty row', () => {
// A canvas node is small; an "Empty" row per undeclared value would crowd out the ones that
// say something. The RAIL is where every declared field appears, filled or not.
mount({
node: { ...MARCUS, fields: {} },
nodeSchema: schemaWith([{ key=[redacted], label: 'Stance', type: 'select', displayOnCard: true }]),
});
expect(screen.queryByText('Stance')).not.toBeInTheDocument();
});
it('renders a field whose TYPE is unknown as text, rather than hiding it', () => {
mount({
node: { ...MARCUS, fields: { mood: 'curious' } },
nodeSchema: schemaWith([{ key=[redacted], label: 'Mood', type: 'sentiment', displayOnCard: true }]),
});
expect(screen.getByText('Mood')).toBeInTheDocument();
expect(screen.getByText('curious')).toBeInTheDocument();
});
});
describe('the three states a node can be in', () => {
it('shows a node whose identity is unresolved by its DOCUMENT id, not as an empty card', () => {
// An empty card is indistinguishable from a rendering bug.
mount({
node: { id: 'n1', documentId: 'doc_unknown', fields: {} },
title: 'doc_unknown',
nodeSchema: schemaWith([]),
});
expect(screen.getByText('doc_unknown')).toBeInTheDocument();
});
it('shows a LINT ring and the finding, not a panel', () => {
// A finding is an observation about a graph mid-build; a node that shouted would make the
// normal state of a half-built graph look broken.
const { container } = mount({
node: MARCUS,
nodeSchema: schemaWith([]),
lint: [{ code: 'isolated_node', message: 'Marcus Lee has no connections on this graph.' }],
});
expect(container.querySelector('.ring-1')).not.toBeNull();
expect(
screen.getByLabelText('Marcus Lee has no connections on this graph.'),
).toBeInTheDocument();
});
it('a MISSING document keeps its cached values and says why', () => {
// There is no foreign key here, so this rendering is the guard that replaces one — and a
// node that went blank would tell you nothing about what it used to say.
mount({
node: { ...MARCUS, derived: { title: 'CTO' } },
nodeSchema: schemaWith([
{ key=[redacted], label: 'Title', type: 'text', from: 'person.title', displayOnCard: true },
]),
missingDocument: true,
});
expect(screen.getByText(/no longer resolves/i)).toBeInTheDocument();
expect(screen.getByText('CTO')).toBeInTheDocument();
});
});
describe('value resolution on the face', () => {
it('prefers a HYDRATED value over the cache', () => {
mount({
node: { ...MARCUS, derived: { title: 'cached CTO' } },
nodeSchema: schemaWith([
{ key=[redacted], label: 'Title', type: 'text', from: 'person.title', displayOnCard: true },
]),
hydrated: { title: 'live CTO' },
});
expect(screen.getByText('live CTO')).toBeInTheDocument();
expect(screen.queryByText('cached CTO')).not.toBeInTheDocument();
});
it('paints the CACHE when hydration has not arrived, so first paint is never blank', () => {
mount({
node: { ...MARCUS, derived: { title: 'cached CTO' } },
nodeSchema: schemaWith([
{ key=[redacted], label: 'Title', type: 'text', from: 'person.title', displayOnCard: true },
]),
});
expect(screen.getByText('cached CTO')).toBeInTheDocument();
});
});
describe('affordances', () => {
it('is a pointer target — clicking it opens the document it points at', () => {
const { container } = mount({ node: MARCUS, nodeSchema: schemaWith([]) });
expect(container.firstElementChild?.className).toContain('cursor-pointer');
});
});