Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
- ✅ `ListViewSchema` Zod schema extended with all new properties
- ✅ ViewConfigPanel aligned to full `ListViewSchema` spec: navigation mode, selection, pagination, export sub-config, searchable/filterable/hidden fields, resizable, density mode, row/bulk actions, sharing, addRecord sub-editor, conditional formatting, quick filters, showRecordCount, allowPrinting, virtualScroll, empty state, ARIA accessibility
- ✅ Semantic fix: `editRecordsInline` → `inlineEdit` field name alignment (i18n keys, data-testid, component label all unified to `inlineEdit`)
- ✅ Semantic fix: `rowHeight` values aligned to full spec — all 5 RowHeight enum values (`compact`/`short`/`medium`/`tall`/`extra_tall`) now supported in NamedListView, ObjectGridSchema, ListViewSchema, Zod schema, and UI
- ✅ Semantic fix: `rowHeight` values aligned to full spec — all 5 RowHeight enum values (`compact`/`short`/`medium`/`tall`/`extra_tall`) now supported in NamedListView, ObjectGridSchema, ListViewSchema, Zod schema, UI, and ObjectGrid rendering (cell classes, cycle toggle, icon mapping)
- ✅ `clickIntoRecordDetails` toggle added to UserActions section (NamedListView spec field — previously only implicit via navigation mode)
- ✅ **Strict spec-order alignment**: All fields within each section reordered to match NamedListView property declaration order:
- PageConfig: showSort before showFilters; allowExport before navigation (per spec)
Expand Down Expand Up @@ -382,7 +382,14 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
- [x] All 122 existing ViewConfigPanel tests pass (test mock updated for ConfigPanelRenderer + useConfigDraft)
- [x] All 23 ObjectView integration tests pass (test ID and title props forwarded)
- [x] 53 new schema-driven tests (utils + schema factory coverage)
- [x] Full affected test suite: 2457 tests across 81 files, all pass
- [x] 14 new ObjectGrid rowHeight tests (all 5 enum values: initialization, cycle, label, toggle visibility)
- [x] Full affected test suite: 2457+ tests across 81+ files, all pass

**Phase 5 — Spec Alignment Completion (Issue #745):**
- [x] ObjectGrid rowHeight: full 5-enum rendering (cellClassName, cycleRowHeight, icon map) — was hardcoded to 3
- [x] 18 new ViewConfigPanel interaction tests: collapseAllByDefault, showDescription, clickIntoRecordDetails, addDeleteRecordsInline toggles; sharing visibility conditional hide; navigation width/openNewTab conditional rendering; all 5 rowHeight button clicks; boundary tests (empty actions, long labels, special chars); pageSizeOptions input; densityMode/ARIA live enums; addRecord conditional sub-editor; sharing visibility select
- [x] 8 new schema-driven spec tests: accessibility field ordering, emptyState compound field, switch field defaults, comprehensive visibleWhen predicates (sharing, navigation width, navigation openNewTab)
- [x] All spec fields verified: Appearance/UserActions/Sharing/Accessibility sections 100% covered with UI controls, defaults, ordering, and conditional visibility

**Code Reduction:** ~1655 lines imperative → ~170 lines declarative wrapper + ~1100 lines schema factory + ~180 lines shared utils = **>50% net reduction in component code** with significantly improved maintainability

Expand Down
337 changes: 337 additions & 0 deletions apps/console/src/__tests__/ViewConfigPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2433,4 +2433,341 @@ describe('ViewConfigPanel', () => {
fireEvent.click(screen.getByTestId('toggle-virtualScroll'));
expect(onViewUpdate).toHaveBeenCalledWith('virtualScroll', true);
});

// ── Spec alignment: toggle interaction tests for all switch fields ──

it('toggles collapseAllByDefault and calls onViewUpdate', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={mockActiveView}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

fireEvent.click(screen.getByTestId('toggle-collapseAllByDefault'));
expect(onViewUpdate).toHaveBeenCalledWith('collapseAllByDefault', true);
});

it('toggles showDescription and calls onViewUpdate', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={mockActiveView}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

fireEvent.click(screen.getByTestId('toggle-showDescription'));
expect(onViewUpdate).toHaveBeenCalledWith('showDescription', false);
});

it('toggles clickIntoRecordDetails and calls onViewUpdate', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={mockActiveView}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

fireEvent.click(screen.getByTestId('toggle-clickIntoRecordDetails'));
expect(onViewUpdate).toHaveBeenCalledWith('clickIntoRecordDetails', false);
});

it('toggles addDeleteRecordsInline and calls onViewUpdate', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={mockActiveView}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

fireEvent.click(screen.getByTestId('toggle-addDeleteRecordsInline'));
expect(onViewUpdate).toHaveBeenCalledWith('addDeleteRecordsInline', false);
});

// ── Conditional rendering: sharing visibility hidden when disabled ──

it('hides sharing visibility select when sharing is not enabled', () => {
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={{ ...mockActiveView, sharing: { enabled: false } }}
objectDef={mockObjectDef}
/>
);

expect(screen.getByTestId('toggle-sharing-enabled')).toBeInTheDocument();
expect(screen.queryByTestId('select-sharing-visibility')).not.toBeInTheDocument();
});

it('hides sharing visibility select when sharing is undefined', () => {
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={mockActiveView}
objectDef={mockObjectDef}
/>
);

expect(screen.queryByTestId('select-sharing-visibility')).not.toBeInTheDocument();
});

// ── Conditional rendering: navigation width hidden when mode is page ──

it('hides navigation width when mode is page', () => {
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={{ ...mockActiveView, navigation: { mode: 'page' } }}
objectDef={mockObjectDef}
/>
);

expect(screen.queryByTestId('input-navigation-width')).not.toBeInTheDocument();
});

it('hides navigation openNewTab when mode is drawer', () => {
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={{ ...mockActiveView, navigation: { mode: 'drawer' } }}
objectDef={mockObjectDef}
/>
);

expect(screen.queryByTestId('toggle-navigation-openNewTab')).not.toBeInTheDocument();
});

// ── All 5 rowHeight buttons: click each value ──

it('clicks all 5 rowHeight buttons and verifies onViewUpdate for each', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={mockActiveView}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

const heights = ['compact', 'short', 'medium', 'tall', 'extra_tall'];
heights.forEach((h) => {
fireEvent.click(screen.getByTestId(`row-height-${h}`));
expect(onViewUpdate).toHaveBeenCalledWith('rowHeight', h);
});
expect(onViewUpdate).toHaveBeenCalledTimes(heights.length);
});

// ── Boundary: empty actions input ──

it('handles empty bulkActions input gracefully', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={{ ...mockActiveView, bulkActions: ['delete'] }}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

fireEvent.click(screen.getByText('console.objectView.bulkActions'));
fireEvent.change(screen.getByTestId('input-bulkActions'), { target: { value: '' } });
expect(onViewUpdate).toHaveBeenCalledWith('bulkActions', []);
});

it('handles empty rowActions input gracefully', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={{ ...mockActiveView, rowActions: ['edit'] }}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

fireEvent.click(screen.getByText('console.objectView.rowActions'));
fireEvent.change(screen.getByTestId('input-rowActions'), { target: { value: '' } });
expect(onViewUpdate).toHaveBeenCalledWith('rowActions', []);
});

// ── Boundary: long label in title input ──

it('handles long label value in view title input', () => {
const longLabel = 'A'.repeat(200);
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={{ ...mockActiveView, label: longLabel }}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

expect(screen.getByTestId('view-title-input')).toHaveValue(longLabel);
});

// ── Boundary: special characters in emptyState fields ──

it('handles special characters in emptyState fields', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={mockActiveView}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

fireEvent.change(screen.getByTestId('input-emptyState-title'), { target: { value: '<script>alert("xss")</script>' } });
expect(onViewUpdate).toHaveBeenCalledWith('emptyState', expect.objectContaining({
title: '<script>alert("xss")</script>',
}));
});

// ── pageSizeOptions input interaction ──

it('updates pageSizeOptions via input', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={mockActiveView}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

const input = screen.getByTestId('input-pagination-pageSizeOptions');
fireEvent.change(input, { target: { value: '10, 25, 50, 100' } });
expect(onViewUpdate).toHaveBeenCalledWith('pagination', expect.objectContaining({
pageSizeOptions: [10, 25, 50, 100],
}));
});

it('filters invalid pageSizeOptions values (non-positive, NaN)', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={mockActiveView}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

const input = screen.getByTestId('input-pagination-pageSizeOptions');
fireEvent.change(input, { target: { value: 'abc, 50, -10, 0' } });
expect(onViewUpdate).toHaveBeenCalledWith('pagination', expect.objectContaining({
pageSizeOptions: [50],
}));
});

// ── Boundary: densityMode enum selection ──

it('changes densityMode to all enum values', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={mockActiveView}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

const select = screen.getByTestId('select-densityMode');
['compact', 'comfortable', 'spacious'].forEach((mode) => {
fireEvent.change(select, { target: { value: mode } });
expect(onViewUpdate).toHaveBeenCalledWith('densityMode', mode);
});
});

// ── Conditional rendering: addRecord sub-editor hidden when not enabled ──

it('hides addRecord sub-editor when addRecordViaForm is false', () => {
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={{ ...mockActiveView, addRecordViaForm: false }}
objectDef={mockObjectDef}
/>
);

expect(screen.queryByTestId('select-addRecord-position')).not.toBeInTheDocument();
});

// ── Sharing visibility select changes value ──

it('changes sharing visibility and calls onViewUpdate', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={{ ...mockActiveView, sharing: { enabled: true, visibility: 'private' } }}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

fireEvent.change(screen.getByTestId('select-sharing-visibility'), { target: { value: 'organization' } });
expect(onViewUpdate).toHaveBeenCalledWith('sharing', expect.objectContaining({
enabled: true,
visibility: 'organization',
}));
});

// ── ARIA live select enum ──

it('changes ARIA live to all enum values', () => {
const onViewUpdate = vi.fn();
render(
<ViewConfigPanel
open={true}
onClose={vi.fn()}
activeView={mockActiveView}
objectDef={mockObjectDef}
onViewUpdate={onViewUpdate}
/>
);

const select = screen.getByTestId('select-aria-live');
['polite', 'assertive', 'off'].forEach((mode) => {
fireEvent.change(select, { target: { value: mode } });
expect(onViewUpdate).toHaveBeenCalledWith('aria', expect.objectContaining({ live: mode }));
});
});
});
Loading