|
1 | 1 | /** |
2 | 2 | * System Admin Pages Integration Tests |
3 | 3 | * |
4 | | - * Tests that system pages (User, Org, Role, AuditLog) fetch data |
5 | | - * via useAdapter() and render records from the API. |
| 4 | + * Tests that system pages render the correct page header and delegate |
| 5 | + * data rendering to the ObjectView component from @object-ui/plugin-view, |
| 6 | + * configured with the appropriate object metadata from systemObjects.ts. |
6 | 7 | */ |
7 | 8 |
|
8 | 9 | import { describe, it, expect, vi, beforeEach } from 'vitest'; |
9 | 10 | import { render, screen, waitFor, fireEvent } from '@testing-library/react'; |
10 | 11 | import '@testing-library/jest-dom'; |
11 | 12 | import { MemoryRouter } from 'react-router-dom'; |
12 | 13 |
|
| 14 | +// --- Capture ObjectView props for assertion --- |
| 15 | +let lastObjectViewProps: any = null; |
| 16 | + |
| 17 | +vi.mock('@object-ui/plugin-view', () => ({ |
| 18 | + ObjectView: (props: any) => { |
| 19 | + lastObjectViewProps = props; |
| 20 | + return ( |
| 21 | + <div |
| 22 | + data-testid="plugin-object-view" |
| 23 | + data-objectname={props.schema?.objectName} |
| 24 | + data-operations={JSON.stringify(props.schema?.operations)} |
| 25 | + /> |
| 26 | + ); |
| 27 | + }, |
| 28 | +})); |
| 29 | + |
13 | 30 | // --- Shared mock adapter --- |
14 | 31 | const mockFind = vi.fn().mockResolvedValue({ data: [], total: 0 }); |
15 | 32 | const mockCreate = vi.fn().mockResolvedValue({ id: 'new-1' }); |
16 | 33 | const mockDelete = vi.fn().mockResolvedValue({}); |
17 | 34 |
|
| 35 | +const mockAdapter = { |
| 36 | + find: mockFind, |
| 37 | + create: mockCreate, |
| 38 | + delete: mockDelete, |
| 39 | + update: vi.fn(), |
| 40 | + findOne: vi.fn(), |
| 41 | + getObjectSchema: vi.fn().mockResolvedValue({ name: 'test', fields: {} }), |
| 42 | +}; |
| 43 | + |
18 | 44 | vi.mock('../context/AdapterProvider', () => ({ |
19 | | - useAdapter: () => ({ |
20 | | - find: mockFind, |
21 | | - create: mockCreate, |
22 | | - delete: mockDelete, |
23 | | - update: vi.fn(), |
24 | | - findOne: vi.fn(), |
25 | | - }), |
| 45 | + useAdapter: () => mockAdapter, |
26 | 46 | })); |
27 | 47 |
|
28 | 48 | vi.mock('@object-ui/auth', () => ({ |
@@ -75,110 +95,88 @@ function wrap(ui: React.ReactElement) { |
75 | 95 |
|
76 | 96 | beforeEach(() => { |
77 | 97 | vi.clearAllMocks(); |
| 98 | + lastObjectViewProps = null; |
78 | 99 | }); |
79 | 100 |
|
80 | 101 | describe('UserManagementPage', () => { |
81 | | - it('should call dataSource.find("sys_user") on mount', async () => { |
82 | | - mockFind.mockResolvedValueOnce({ |
83 | | - data: [{ id: '1', name: 'Alice', email: 'alice@test.com', role: 'admin', status: 'active', lastLoginAt: '' }], |
84 | | - }); |
| 102 | + it('should render ObjectView with sys_user object and page header', () => { |
85 | 103 | wrap(<UserManagementPage />); |
86 | | - await waitFor(() => { |
87 | | - expect(mockFind).toHaveBeenCalledWith('sys_user'); |
88 | | - }); |
89 | | - await waitFor(() => { |
90 | | - expect(screen.getByText('Alice')).toBeInTheDocument(); |
91 | | - }); |
| 104 | + expect(screen.getByText('User Management')).toBeInTheDocument(); |
| 105 | + expect(screen.getByText('Manage system users and their roles')).toBeInTheDocument(); |
| 106 | + expect(screen.getByTestId('plugin-object-view')).toBeInTheDocument(); |
| 107 | + expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_user'); |
92 | 108 | }); |
93 | 109 |
|
94 | | - it('should show empty state when no users', async () => { |
95 | | - mockFind.mockResolvedValueOnce({ data: [] }); |
| 110 | + it('should pass the adapter as dataSource to ObjectView', () => { |
96 | 111 | wrap(<UserManagementPage />); |
97 | | - await waitFor(() => { |
98 | | - expect(screen.getByText('No users found.')).toBeInTheDocument(); |
99 | | - }); |
| 112 | + expect(lastObjectViewProps.dataSource).toBe(mockAdapter); |
100 | 113 | }); |
101 | 114 |
|
102 | | - it('should call create when Add User is clicked', async () => { |
103 | | - mockFind.mockResolvedValue({ data: [] }); |
104 | | - mockCreate.mockResolvedValueOnce({ id: 'new-user' }); |
| 115 | + it('should enable CRUD operations for admin users', () => { |
105 | 116 | wrap(<UserManagementPage />); |
106 | | - await waitFor(() => { |
107 | | - expect(screen.getByText('No users found.')).toBeInTheDocument(); |
108 | | - }); |
109 | | - fireEvent.click(screen.getByText('Add User')); |
110 | | - await waitFor(() => { |
111 | | - expect(mockCreate).toHaveBeenCalledWith('sys_user', expect.objectContaining({ name: 'New User' })); |
112 | | - }); |
| 117 | + const ops = lastObjectViewProps.schema.operations; |
| 118 | + expect(ops).toEqual({ create: true, update: true, delete: true }); |
| 119 | + }); |
| 120 | + |
| 121 | + it('should configure table columns from systemObjects metadata', () => { |
| 122 | + wrap(<UserManagementPage />); |
| 123 | + expect(lastObjectViewProps.schema.table.columns).toEqual( |
| 124 | + ['name', 'email', 'role', 'status', 'lastLoginAt'] |
| 125 | + ); |
113 | 126 | }); |
114 | 127 | }); |
115 | 128 |
|
116 | 129 | describe('OrgManagementPage', () => { |
117 | | - it('should call dataSource.find("sys_org") on mount', async () => { |
118 | | - mockFind.mockResolvedValueOnce({ |
119 | | - data: [{ id: '1', name: 'Acme', slug: 'acme', plan: 'pro', status: 'active', memberCount: 5 }], |
120 | | - }); |
| 130 | + it('should render ObjectView with sys_org object and page header', () => { |
121 | 131 | wrap(<OrgManagementPage />); |
122 | | - await waitFor(() => { |
123 | | - expect(mockFind).toHaveBeenCalledWith('sys_org'); |
124 | | - }); |
125 | | - await waitFor(() => { |
126 | | - expect(screen.getByText('Acme')).toBeInTheDocument(); |
127 | | - }); |
| 132 | + expect(screen.getByText('Organization Management')).toBeInTheDocument(); |
| 133 | + expect(screen.getByText('Manage organizations and their members')).toBeInTheDocument(); |
| 134 | + expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_org'); |
128 | 135 | }); |
129 | 136 |
|
130 | | - it('should show empty state when no organizations', async () => { |
131 | | - mockFind.mockResolvedValueOnce({ data: [] }); |
| 137 | + it('should configure table columns from systemObjects metadata', () => { |
132 | 138 | wrap(<OrgManagementPage />); |
133 | | - await waitFor(() => { |
134 | | - expect(screen.getByText('No organizations found.')).toBeInTheDocument(); |
135 | | - }); |
| 139 | + expect(lastObjectViewProps.schema.table.columns).toEqual( |
| 140 | + ['name', 'slug', 'plan', 'status', 'memberCount'] |
| 141 | + ); |
136 | 142 | }); |
137 | 143 | }); |
138 | 144 |
|
139 | 145 | describe('RoleManagementPage', () => { |
140 | | - it('should call dataSource.find("sys_role") on mount', async () => { |
141 | | - mockFind.mockResolvedValueOnce({ |
142 | | - data: [{ id: '1', name: 'Admin', description: 'Full access', isSystem: true, userCount: 3 }], |
143 | | - }); |
| 146 | + it('should render ObjectView with sys_role object and page header', () => { |
144 | 147 | wrap(<RoleManagementPage />); |
145 | | - await waitFor(() => { |
146 | | - expect(mockFind).toHaveBeenCalledWith('sys_role'); |
147 | | - }); |
148 | | - await waitFor(() => { |
149 | | - expect(screen.getByText('Admin')).toBeInTheDocument(); |
150 | | - }); |
| 148 | + expect(screen.getByText('Role Management')).toBeInTheDocument(); |
| 149 | + expect(screen.getByText('Define roles and assign permissions')).toBeInTheDocument(); |
| 150 | + expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_role'); |
151 | 151 | }); |
152 | 152 |
|
153 | | - it('should show empty state when no roles', async () => { |
154 | | - mockFind.mockResolvedValueOnce({ data: [] }); |
| 153 | + it('should configure table columns from systemObjects metadata', () => { |
155 | 154 | wrap(<RoleManagementPage />); |
156 | | - await waitFor(() => { |
157 | | - expect(screen.getByText('No roles found.')).toBeInTheDocument(); |
158 | | - }); |
| 155 | + expect(lastObjectViewProps.schema.table.columns).toEqual( |
| 156 | + ['name', 'description', 'isSystem', 'userCount'] |
| 157 | + ); |
159 | 158 | }); |
160 | 159 | }); |
161 | 160 |
|
162 | 161 | describe('AuditLogPage', () => { |
163 | | - it('should call dataSource.find("sys_audit_log") on mount', async () => { |
164 | | - mockFind.mockResolvedValueOnce({ |
165 | | - data: [{ id: '1', action: 'create', resource: 'user', userName: 'Admin', ipAddress: '127.0.0.1', createdAt: '2026-01-01' }], |
166 | | - }); |
| 162 | + it('should render ObjectView with sys_audit_log object and page header', () => { |
167 | 163 | wrap(<AuditLogPage />); |
168 | | - await waitFor(() => { |
169 | | - expect(mockFind).toHaveBeenCalledWith('sys_audit_log', expect.objectContaining({ $orderby: { createdAt: 'desc' } })); |
170 | | - }); |
171 | | - await waitFor(() => { |
172 | | - expect(screen.getByText('create')).toBeInTheDocument(); |
173 | | - }); |
| 164 | + expect(screen.getByText('Audit Log')).toBeInTheDocument(); |
| 165 | + expect(screen.getByText('View system activity and user actions')).toBeInTheDocument(); |
| 166 | + expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_audit_log'); |
174 | 167 | }); |
175 | 168 |
|
176 | | - it('should show empty state when no logs', async () => { |
177 | | - mockFind.mockResolvedValueOnce({ data: [] }); |
| 169 | + it('should disable all mutation operations (read-only)', () => { |
178 | 170 | wrap(<AuditLogPage />); |
179 | | - await waitFor(() => { |
180 | | - expect(screen.getByText('No audit logs found.')).toBeInTheDocument(); |
181 | | - }); |
| 171 | + const ops = lastObjectViewProps.schema.operations; |
| 172 | + expect(ops).toEqual({ create: false, update: false, delete: false }); |
| 173 | + }); |
| 174 | + |
| 175 | + it('should configure table columns from systemObjects metadata', () => { |
| 176 | + wrap(<AuditLogPage />); |
| 177 | + expect(lastObjectViewProps.schema.table.columns).toEqual( |
| 178 | + ['action', 'resource', 'userName', 'ipAddress', 'createdAt'] |
| 179 | + ); |
182 | 180 | }); |
183 | 181 | }); |
184 | 182 |
|
@@ -250,56 +248,29 @@ describe('AppManagementPage', () => { |
250 | 248 | }); |
251 | 249 |
|
252 | 250 | describe('PermissionManagementPage', () => { |
253 | | - it('should call dataSource.find("sys_permission") on mount', async () => { |
254 | | - mockFind.mockResolvedValueOnce({ |
255 | | - data: [{ id: '1', name: 'manage_users', resource: 'user', action: 'manage', description: 'Full user access' }], |
256 | | - }); |
| 251 | + it('should render ObjectView with sys_permission object and page header', () => { |
257 | 252 | wrap(<PermissionManagementPage />); |
258 | | - await waitFor(() => { |
259 | | - expect(mockFind).toHaveBeenCalledWith('sys_permission'); |
260 | | - }); |
261 | | - await waitFor(() => { |
262 | | - expect(screen.getByText('manage_users')).toBeInTheDocument(); |
263 | | - }); |
| 253 | + expect(screen.getByText('Permissions')).toBeInTheDocument(); |
| 254 | + expect(screen.getByText('Manage permission rules and assignments')).toBeInTheDocument(); |
| 255 | + expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_permission'); |
264 | 256 | }); |
265 | 257 |
|
266 | | - it('should show empty state when no permissions', async () => { |
267 | | - mockFind.mockResolvedValueOnce({ data: [] }); |
| 258 | + it('should enable CRUD operations for admin users', () => { |
268 | 259 | wrap(<PermissionManagementPage />); |
269 | | - await waitFor(() => { |
270 | | - expect(screen.getByText('No permissions found.')).toBeInTheDocument(); |
271 | | - }); |
| 260 | + const ops = lastObjectViewProps.schema.operations; |
| 261 | + expect(ops).toEqual({ create: true, update: true, delete: true }); |
272 | 262 | }); |
273 | 263 |
|
274 | | - it('should call create when Add Permission is clicked', async () => { |
275 | | - mockFind.mockResolvedValue({ data: [] }); |
276 | | - mockCreate.mockResolvedValueOnce({ id: 'new-perm' }); |
| 264 | + it('should configure table columns from systemObjects metadata', () => { |
277 | 265 | wrap(<PermissionManagementPage />); |
278 | | - await waitFor(() => { |
279 | | - expect(screen.getByText('No permissions found.')).toBeInTheDocument(); |
280 | | - }); |
281 | | - fireEvent.click(screen.getByText('Add Permission')); |
282 | | - await waitFor(() => { |
283 | | - expect(mockCreate).toHaveBeenCalledWith('sys_permission', expect.objectContaining({ name: 'New Permission' })); |
284 | | - }); |
| 266 | + expect(lastObjectViewProps.schema.table.columns).toEqual( |
| 267 | + ['name', 'resource', 'action', 'description'] |
| 268 | + ); |
285 | 269 | }); |
286 | 270 |
|
287 | | - it('should filter permissions by search query', async () => { |
288 | | - mockFind.mockResolvedValue({ |
289 | | - data: [ |
290 | | - { id: '1', name: 'manage_users', resource: 'user', action: 'manage', description: '' }, |
291 | | - { id: '2', name: 'read_reports', resource: 'report', action: 'read', description: '' }, |
292 | | - ], |
293 | | - }); |
| 271 | + it('should enable search and filters', () => { |
294 | 272 | wrap(<PermissionManagementPage />); |
295 | | - await waitFor(() => { |
296 | | - expect(screen.getByText('manage_users')).toBeInTheDocument(); |
297 | | - expect(screen.getByText('read_reports')).toBeInTheDocument(); |
298 | | - }); |
299 | | - fireEvent.change(screen.getByTestId('permission-search-input'), { target: { value: 'report' } }); |
300 | | - await waitFor(() => { |
301 | | - expect(screen.queryByText('manage_users')).not.toBeInTheDocument(); |
302 | | - expect(screen.getByText('read_reports')).toBeInTheDocument(); |
303 | | - }); |
| 273 | + expect(lastObjectViewProps.schema.showSearch).toBe(true); |
| 274 | + expect(lastObjectViewProps.schema.showFilters).toBe(true); |
304 | 275 | }); |
305 | 276 | }); |
0 commit comments