|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * The flow inspector's help lines (#27356 row 15). |
| 5 | + * |
| 6 | + * 1. A schema description on an objectList column is the column's help, shown |
| 7 | + * under the column, and never its placeholder: a paragraph squeezed into |
| 8 | + * an input's placeholder reads as a cut-off example value and vanishes on |
| 9 | + * the first keystroke. |
| 10 | + * 2. Help longer than `FLOW_HELP_CLAMP_CHARS` opens clamped to two lines with a |
| 11 | + * "Learn more" toggle; shorter help renders in full with no toggle. The |
| 12 | + * full text is in the DOM either way. |
| 13 | + */ |
| 14 | + |
| 15 | +import { describe, it, expect, vi, afterEach } from 'vitest'; |
| 16 | +import { render, screen, cleanup, fireEvent, within } from '@testing-library/react'; |
| 17 | +import { z } from 'zod'; |
| 18 | +import { ApprovalNodeConfigSchema } from '@objectstack/spec/automation'; |
| 19 | +import { FlowObjectListField } from './FlowObjectListField'; |
| 20 | +import { FlowNodeConfigField } from './FlowNodeConfigField'; |
| 21 | +import { FLOW_HELP_CLAMP_CHARS } from './FlowFieldHelp'; |
| 22 | +import { jsonSchemaToFlowFields } from './json-schema-to-fields'; |
| 23 | +import type { FlowConfigField } from './flow-node-config'; |
| 24 | + |
| 25 | +afterEach(cleanup); |
| 26 | + |
| 27 | +// The approval node's fields as the engine publishes them: the installed |
| 28 | +// contract's own JSON Schema, not a copy of it. |
| 29 | +const schema = z.toJSONSchema(ApprovalNodeConfigSchema, { io: 'input' }); |
| 30 | +const served = jsonSchemaToFlowFields(schema)!; |
| 31 | +const approvers = served.find((f) => f.id === 'approvers')!; |
| 32 | +const orgCol = approvers.columns!.find((c) => c.key === 'organization')!; |
| 33 | +const orgDescription = ( |
| 34 | + schema as unknown as { |
| 35 | + properties: { approvers: { items: { properties: { organization: { description: string } } } } }; |
| 36 | + } |
| 37 | +).properties.approvers.items.properties.organization.description; |
| 38 | + |
| 39 | +describe('objectList column descriptions are help, not placeholders (#27356 row 15)', () => { |
| 40 | + it('maps the served approver `organization` description to the column help', () => { |
| 41 | + expect(orgDescription.length).toBeGreaterThan(0); |
| 42 | + expect(orgCol.help).toBe(orgDescription); |
| 43 | + expect(orgCol.placeholder).toBeUndefined(); |
| 44 | + }); |
| 45 | + |
| 46 | + it('renders the description under the column, with no input carrying it as a placeholder', () => { |
| 47 | + const { container } = render( |
| 48 | + <FlowObjectListField |
| 49 | + label="Approvers" |
| 50 | + columns={approvers.columns!} |
| 51 | + value={[{ type: 'manager' }]} |
| 52 | + onCommit={vi.fn()} |
| 53 | + addLabel="Add" |
| 54 | + removeLabel="Remove" |
| 55 | + emptyLabel="None" |
| 56 | + />, |
| 57 | + ); |
| 58 | + const placeholders = Array.from(container.querySelectorAll('[placeholder]')).map((el) => el.getAttribute('placeholder')); |
| 59 | + expect(placeholders).not.toContain(orgDescription); |
| 60 | + const helps = screen.getAllByTestId('flow-field-help').map((el) => el.querySelector('p')?.textContent); |
| 61 | + expect(helps).toContain(orgDescription); |
| 62 | + }); |
| 63 | +}); |
| 64 | + |
| 65 | +const field = (help: string): FlowConfigField => ({ |
| 66 | + id: 'excludeSubmitter', |
| 67 | + path: ['config', 'excludeSubmitter'], |
| 68 | + label: 'Exclude submitter', |
| 69 | + kind: 'boolean', |
| 70 | + help, |
| 71 | +}); |
| 72 | + |
| 73 | +describe('long help opens clamped behind "Learn more" (#27356 row 15)', () => { |
| 74 | + it('clamps a long help and toggles it open and closed', () => { |
| 75 | + const long = 'Segregation of duties for this step. '.repeat(5).trim(); |
| 76 | + expect(long.length).toBeGreaterThan(FLOW_HELP_CLAMP_CHARS); |
| 77 | + render(<FlowNodeConfigField field={field(long)} value={undefined} onCommit={() => {}} />); |
| 78 | + const help = screen.getByTestId('flow-field-help'); |
| 79 | + const text = help.querySelector('p')!; |
| 80 | + expect(text.textContent).toBe(long); |
| 81 | + expect(text.className).toContain('line-clamp-2'); |
| 82 | + const toggle = within(help).getByRole('button', { name: 'Learn more' }); |
| 83 | + expect(toggle.getAttribute('aria-expanded')).toBe('false'); |
| 84 | + expect(toggle.getAttribute('aria-controls')).toBe(text.id); |
| 85 | + |
| 86 | + fireEvent.click(toggle); |
| 87 | + expect(text.className).not.toContain('line-clamp-2'); |
| 88 | + const less = within(help).getByRole('button', { name: 'Show less' }); |
| 89 | + expect(less.getAttribute('aria-expanded')).toBe('true'); |
| 90 | + |
| 91 | + fireEvent.click(less); |
| 92 | + expect(text.className).toContain('line-clamp-2'); |
| 93 | + }); |
| 94 | + |
| 95 | + it('renders a short help in full, with no toggle', () => { |
| 96 | + render(<FlowNodeConfigField field={field('Lock the record while pending.')} value={undefined} onCommit={() => {}} />); |
| 97 | + const help = screen.getByTestId('flow-field-help'); |
| 98 | + expect(help.querySelector('p')!.className).not.toContain('line-clamp-2'); |
| 99 | + expect(within(help).queryByRole('button')).toBeNull(); |
| 100 | + }); |
| 101 | +}); |
0 commit comments