@object-ui/react 3.1.5 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +28 -3
- package/dist/SchemaRenderer.js +2 -2
- package/dist/components/form/FieldFactory.js +5 -2
- package/dist/context/AppShellContext.d.ts +26 -0
- package/dist/context/AppShellContext.js +67 -0
- package/dist/context/index.d.ts +1 -7
- package/dist/context/index.js +1 -7
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useDataRefresh.d.ts +36 -0
- package/dist/hooks/useDataRefresh.js +50 -0
- package/dist/hooks/useDiscovery.d.ts +12 -0
- package/dist/hooks/useDiscovery.js +6 -0
- package/dist/hooks/usePerformanceBudget.js +1 -1
- package/package.json +32 -9
- package/.turbo/turbo-build.log +0 -4
- package/src/LazyPluginLoader.tsx +0 -174
- package/src/SchemaRenderer.tsx +0 -266
- package/src/__tests__/LazyPluginLoader.test.tsx +0 -176
- package/src/__tests__/SchemaRenderer.aria.test.tsx +0 -118
- package/src/__tests__/SchemaRenderer.debug.test.tsx +0 -69
- package/src/__tests__/SchemaRenderer.errorBoundary.test.tsx +0 -114
- package/src/__tests__/SchemaRenderer.expressions.test.tsx +0 -131
- package/src/__tests__/SchemaRendererProvider.smoke.test.tsx +0 -132
- package/src/__tests__/ThemeProvider.test.tsx +0 -451
- package/src/__tests__/useNavigationOverlay.test.ts +0 -578
- package/src/__tests__/useOffline.test.ts +0 -198
- package/src/__tests__/usePageTransition.test.ts +0 -214
- package/src/__tests__/usePageVariables.test.tsx +0 -334
- package/src/__tests__/usePerformance.test.ts +0 -137
- package/src/__tests__/useViewData.test.tsx +0 -422
- package/src/__tests__/view-switching.test.tsx +0 -456
- package/src/components/form/FieldFactory.tsx +0 -509
- package/src/components/form/FormRenderer.test.tsx +0 -744
- package/src/components/form/FormRenderer.tsx +0 -258
- package/src/components/form/README.md +0 -352
- package/src/components/form/index.ts +0 -12
- package/src/context/ActionContext.tsx +0 -213
- package/src/context/DndContext.tsx +0 -260
- package/src/context/NotificationContext.tsx +0 -280
- package/src/context/SchemaRendererContext.tsx +0 -48
- package/src/context/ThemeContext.tsx +0 -325
- package/src/context/__tests__/ActionContext.test.tsx +0 -174
- package/src/context/__tests__/DndContext.test.tsx +0 -150
- package/src/context/__tests__/NotificationContext.test.tsx +0 -176
- package/src/context/__tests__/useDataScope.test.tsx +0 -91
- package/src/context/index.ts +0 -13
- package/src/hooks/__tests__/useActionEngine.test.ts +0 -170
- package/src/hooks/__tests__/useActionRunner.test.ts +0 -153
- package/src/hooks/__tests__/useAnimation.test.ts +0 -82
- package/src/hooks/__tests__/useColumnSummary.test.ts +0 -93
- package/src/hooks/__tests__/useCrudShortcuts.test.ts +0 -85
- package/src/hooks/__tests__/useDebugMode.test.ts +0 -83
- package/src/hooks/__tests__/useDensityMode.test.ts +0 -81
- package/src/hooks/__tests__/useDiscovery.test.tsx +0 -149
- package/src/hooks/__tests__/useDynamicApp.test.ts +0 -161
- package/src/hooks/__tests__/useETagCache.test.ts +0 -153
- package/src/hooks/__tests__/useExpression.test.ts +0 -86
- package/src/hooks/__tests__/useFocusTrap.test.ts +0 -294
- package/src/hooks/__tests__/useKeyboardShortcuts.test.ts +0 -144
- package/src/hooks/__tests__/usePageTransition.test.ts +0 -158
- package/src/hooks/__tests__/usePerformanceBudget.test.ts +0 -157
- package/src/hooks/__tests__/useReducedMotion.test.ts +0 -104
- package/src/hooks/__tests__/useSchemaPersistence.test.ts +0 -220
- package/src/hooks/__tests__/useViewSharing.test.ts +0 -121
- package/src/hooks/__tests__/useViewTransition.test.ts +0 -61
- package/src/hooks/index.ts +0 -35
- package/src/hooks/useActionEngine.ts +0 -84
- package/src/hooks/useActionRunner.ts +0 -119
- package/src/hooks/useAnimation.ts +0 -117
- package/src/hooks/useClientNotifications.ts +0 -251
- package/src/hooks/useColumnSummary.ts +0 -87
- package/src/hooks/useCrudShortcuts.ts +0 -126
- package/src/hooks/useDebugMode.ts +0 -95
- package/src/hooks/useDensityMode.ts +0 -102
- package/src/hooks/useDiscovery.ts +0 -154
- package/src/hooks/useDynamicApp.ts +0 -142
- package/src/hooks/useETagCache.ts +0 -404
- package/src/hooks/useExpression.ts +0 -56
- package/src/hooks/useFocusManagement.ts +0 -248
- package/src/hooks/useFocusTrap.ts +0 -138
- package/src/hooks/useGlobalUndo.ts +0 -98
- package/src/hooks/useKeyboardShortcuts.ts +0 -119
- package/src/hooks/useNavigationOverlay.ts +0 -190
- package/src/hooks/useOffline.ts +0 -301
- package/src/hooks/usePageTransition.ts +0 -187
- package/src/hooks/usePageVariables.tsx +0 -157
- package/src/hooks/usePerformance.ts +0 -246
- package/src/hooks/usePerformanceBudget.ts +0 -313
- package/src/hooks/useReducedMotion.ts +0 -53
- package/src/hooks/useSchemaPersistence.ts +0 -222
- package/src/hooks/useViewData.ts +0 -201
- package/src/hooks/useViewSharing.ts +0 -140
- package/src/hooks/useViewTransition.ts +0 -144
- package/src/index.ts +0 -41
- package/src/spec-bridge/SpecBridge.ts +0 -64
- package/src/spec-bridge/__tests__/P1SpecBridge.test.ts +0 -855
- package/src/spec-bridge/__tests__/SpecBridge.test.ts +0 -550
- package/src/spec-bridge/bridges/dashboard.ts +0 -180
- package/src/spec-bridge/bridges/form-view.ts +0 -109
- package/src/spec-bridge/bridges/list-view.ts +0 -187
- package/src/spec-bridge/bridges/page.ts +0 -136
- package/src/spec-bridge/index.ts +0 -14
- package/src/spec-bridge/types.ts +0 -38
- package/src/utils/i18n.ts +0 -10
- package/tsconfig.json +0 -17
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -2
package/src/SchemaRenderer.tsx
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import React, { forwardRef, useContext, useMemo, Component } from 'react';
|
|
10
|
-
import {
|
|
11
|
-
SchemaNode,
|
|
12
|
-
ComponentRegistry,
|
|
13
|
-
ExpressionEvaluator,
|
|
14
|
-
isObjectUIError,
|
|
15
|
-
type ObjectUIError,
|
|
16
|
-
ERROR_CODES,
|
|
17
|
-
debugLog,
|
|
18
|
-
debugTime,
|
|
19
|
-
debugTimeEnd,
|
|
20
|
-
DebugCollector,
|
|
21
|
-
} from '@object-ui/core';
|
|
22
|
-
import { SchemaRendererContext } from './context/SchemaRendererContext';
|
|
23
|
-
import { resolveI18nLabel } from './utils/i18n';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Extract AriaPropsSchema properties from a schema node and convert
|
|
27
|
-
* them to standard HTML ARIA attributes.
|
|
28
|
-
*
|
|
29
|
-
* @objectstack/spec AriaPropsSchema defines:
|
|
30
|
-
* ariaLabel: string | I18nLabel (→ aria-label)
|
|
31
|
-
* ariaDescribedBy: string (→ aria-describedby)
|
|
32
|
-
* role: string (→ role)
|
|
33
|
-
*/
|
|
34
|
-
function resolveAriaProps(schema: Record<string, any>): Record<string, string | undefined> {
|
|
35
|
-
const aria: Record<string, string | undefined> = {};
|
|
36
|
-
if (schema.ariaLabel) {
|
|
37
|
-
aria['aria-label'] = resolveI18nLabel(schema.ariaLabel);
|
|
38
|
-
}
|
|
39
|
-
if (schema.ariaDescribedBy) {
|
|
40
|
-
aria['aria-describedby'] = schema.ariaDescribedBy;
|
|
41
|
-
}
|
|
42
|
-
if (schema.role) {
|
|
43
|
-
aria['role'] = schema.role;
|
|
44
|
-
}
|
|
45
|
-
return aria;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Per-component Error Boundary for SchemaRenderer.
|
|
50
|
-
* Catches render errors in individual components, preventing one broken
|
|
51
|
-
* component from crashing the entire page.
|
|
52
|
-
*/
|
|
53
|
-
interface SchemaErrorBoundaryState {
|
|
54
|
-
hasError: boolean;
|
|
55
|
-
error: Error | null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export class SchemaErrorBoundary extends Component<
|
|
59
|
-
{ componentType?: string; children: React.ReactNode },
|
|
60
|
-
SchemaErrorBoundaryState
|
|
61
|
-
> {
|
|
62
|
-
state: SchemaErrorBoundaryState = { hasError: false, error: null };
|
|
63
|
-
|
|
64
|
-
static getDerivedStateFromError(error: Error): SchemaErrorBoundaryState {
|
|
65
|
-
return { hasError: true, error };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
handleRetry = () => {
|
|
69
|
-
this.setState({ hasError: false, error: null });
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
render() {
|
|
73
|
-
if (this.state.hasError && this.state.error) {
|
|
74
|
-
const error = this.state.error;
|
|
75
|
-
const isDev = process.env.NODE_ENV !== 'production';
|
|
76
|
-
const objuiError = isObjectUIError(error) ? error as ObjectUIError : null;
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<div className="p-4 border border-orange-400 rounded bg-orange-50 text-orange-700 my-2" role="alert">
|
|
80
|
-
<p className="font-medium">
|
|
81
|
-
Component{this.props.componentType ? ` "${this.props.componentType}"` : ''} failed to render
|
|
82
|
-
</p>
|
|
83
|
-
<p className="text-sm mt-1">{error.message}</p>
|
|
84
|
-
{isDev && objuiError?.code && (
|
|
85
|
-
<p className="text-xs mt-1 text-orange-500">
|
|
86
|
-
Error code: {objuiError.code}
|
|
87
|
-
{objuiError.details?.suggestion ? (
|
|
88
|
-
<span className="block mt-0.5">💡 {String(objuiError.details.suggestion)}</span>
|
|
89
|
-
) : null}
|
|
90
|
-
</p>
|
|
91
|
-
)}
|
|
92
|
-
<button
|
|
93
|
-
onClick={this.handleRetry}
|
|
94
|
-
className="mt-2 text-sm underline hover:no-underline"
|
|
95
|
-
>
|
|
96
|
-
Retry
|
|
97
|
-
</button>
|
|
98
|
-
</div>
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
return this.props.children;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export const SchemaRenderer = forwardRef<any, { schema: SchemaNode } & Record<string, any>>(({ schema, ...props }, _ref) => {
|
|
106
|
-
const context = useContext(SchemaRendererContext);
|
|
107
|
-
const dataSource = context?.dataSource || {};
|
|
108
|
-
|
|
109
|
-
// Evaluate schema expressions against the data source
|
|
110
|
-
const evaluatedSchema = useMemo(() => {
|
|
111
|
-
if (!schema || typeof schema === 'string') return schema;
|
|
112
|
-
|
|
113
|
-
const evaluator = new ExpressionEvaluator({ data: dataSource });
|
|
114
|
-
// Shallow copy
|
|
115
|
-
const newSchema = { ...schema };
|
|
116
|
-
|
|
117
|
-
// COMPAT: Hoist 'properties' up to schema level
|
|
118
|
-
// This allows support for strict configs that wrap all props in 'properties'
|
|
119
|
-
if (newSchema.properties) {
|
|
120
|
-
Object.assign(newSchema, newSchema.properties);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Evaluate 'content' (common in Text, Button)
|
|
124
|
-
if (typeof newSchema.content === 'string') {
|
|
125
|
-
newSchema.content = evaluator.evaluate(newSchema.content);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Evaluate 'props'
|
|
129
|
-
if (newSchema.props) {
|
|
130
|
-
const newProps = { ...newSchema.props };
|
|
131
|
-
for (const [key, val] of Object.entries(newProps)) {
|
|
132
|
-
newProps[key] = evaluator.evaluate(val as any);
|
|
133
|
-
}
|
|
134
|
-
newSchema.props = newProps;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Evaluate visibility: visible / visibleOn / hidden / hiddenOn
|
|
138
|
-
const shouldHide = (() => {
|
|
139
|
-
if (newSchema.visible !== undefined) {
|
|
140
|
-
return !evaluator.evaluateCondition(newSchema.visible);
|
|
141
|
-
}
|
|
142
|
-
if (newSchema.visibleOn !== undefined) {
|
|
143
|
-
return !evaluator.evaluateCondition(newSchema.visibleOn);
|
|
144
|
-
}
|
|
145
|
-
if (newSchema.hidden !== undefined) {
|
|
146
|
-
return evaluator.evaluateCondition(newSchema.hidden);
|
|
147
|
-
}
|
|
148
|
-
if (newSchema.hiddenOn !== undefined) {
|
|
149
|
-
return evaluator.evaluateCondition(newSchema.hiddenOn);
|
|
150
|
-
}
|
|
151
|
-
return false;
|
|
152
|
-
})();
|
|
153
|
-
|
|
154
|
-
if (shouldHide) {
|
|
155
|
-
newSchema._hidden = true;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Evaluate disabled: disabled / disabledOn
|
|
159
|
-
const isDisabled = (() => {
|
|
160
|
-
if (newSchema.disabled !== undefined) {
|
|
161
|
-
return evaluator.evaluateCondition(newSchema.disabled);
|
|
162
|
-
}
|
|
163
|
-
if (newSchema.disabledOn !== undefined) {
|
|
164
|
-
return evaluator.evaluateCondition(newSchema.disabledOn);
|
|
165
|
-
}
|
|
166
|
-
return false;
|
|
167
|
-
})();
|
|
168
|
-
|
|
169
|
-
if (isDisabled) {
|
|
170
|
-
newSchema._disabled = true;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return newSchema;
|
|
174
|
-
}, [schema, dataSource]);
|
|
175
|
-
|
|
176
|
-
if (!evaluatedSchema) return null;
|
|
177
|
-
// Handle visibility: if evaluated schema is hidden, render nothing
|
|
178
|
-
if (evaluatedSchema?._hidden) return null;
|
|
179
|
-
// If schema is just a string, render it as text
|
|
180
|
-
if (typeof evaluatedSchema === 'string') return <>{evaluatedSchema}</>;
|
|
181
|
-
|
|
182
|
-
debugLog('schema', 'Rendering schema node', { type: evaluatedSchema.type, id: evaluatedSchema.id });
|
|
183
|
-
|
|
184
|
-
const Component = ComponentRegistry.get(evaluatedSchema.type);
|
|
185
|
-
|
|
186
|
-
if (!Component) {
|
|
187
|
-
debugLog('schema', 'Component not found in registry', { type: evaluatedSchema.type });
|
|
188
|
-
const errorInfo = ERROR_CODES['OBJUI-001'];
|
|
189
|
-
return (
|
|
190
|
-
<div className="p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2" role="alert">
|
|
191
|
-
<p className="font-medium">Unknown component type: <strong>{evaluatedSchema.type}</strong></p>
|
|
192
|
-
{process.env.NODE_ENV !== 'production' && (
|
|
193
|
-
<p className="text-xs mt-1">💡 {errorInfo.suggestion} (OBJUI-001)</p>
|
|
194
|
-
)}
|
|
195
|
-
<pre className="text-xs mt-2 overflow-auto">{JSON.stringify(evaluatedSchema, null, 2)}</pre>
|
|
196
|
-
</div>
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Note: We don't forward the ref to the Component because components in the registry
|
|
201
|
-
// may not support refs. The SchemaRenderer itself can still receive refs for its own use.
|
|
202
|
-
|
|
203
|
-
// Extract schema metadata properties that should NOT be passed as React props
|
|
204
|
-
const {
|
|
205
|
-
type: _type,
|
|
206
|
-
children: _children,
|
|
207
|
-
body: _body,
|
|
208
|
-
schema: _schema,
|
|
209
|
-
visible: _visible,
|
|
210
|
-
visibleOn: _visibleOn,
|
|
211
|
-
hidden: _hidden,
|
|
212
|
-
hiddenOn: _hiddenOn,
|
|
213
|
-
disabled: _disabled,
|
|
214
|
-
disabledOn: _disabledOn,
|
|
215
|
-
_hidden: __hidden, // stripped: internal visibility flag
|
|
216
|
-
_disabled: __disabled, // stripped: internal disabled flag
|
|
217
|
-
...componentProps
|
|
218
|
-
} = evaluatedSchema;
|
|
219
|
-
|
|
220
|
-
// Extract AriaPropsSchema properties for accessibility
|
|
221
|
-
const ariaProps = resolveAriaProps(evaluatedSchema);
|
|
222
|
-
|
|
223
|
-
// Debug-mode enhancements: extra data attributes + perf tracking
|
|
224
|
-
const isDebug = context?.debug || context?.debugFlags?.enabled;
|
|
225
|
-
const debugAttrs: Record<string, string> = {};
|
|
226
|
-
if (isDebug) {
|
|
227
|
-
debugAttrs['data-debug-type'] = evaluatedSchema.type;
|
|
228
|
-
if (evaluatedSchema.id) {
|
|
229
|
-
debugAttrs['data-debug-id'] = evaluatedSchema.id;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
debugTime(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
|
|
234
|
-
const renderStart = isDebug ? performance.now() : 0;
|
|
235
|
-
const rendered = (
|
|
236
|
-
<SchemaErrorBoundary componentType={evaluatedSchema.type}>
|
|
237
|
-
{React.createElement(Component, {
|
|
238
|
-
schema: evaluatedSchema,
|
|
239
|
-
...componentProps, // Spread non-metadata schema properties as props
|
|
240
|
-
...(evaluatedSchema.props || {}), // Override with explicit props if provided
|
|
241
|
-
...ariaProps, // Inject ARIA attributes from AriaPropsSchema
|
|
242
|
-
...debugAttrs, // Debug-mode data attributes
|
|
243
|
-
disabled: __disabled || undefined,
|
|
244
|
-
className: evaluatedSchema.className,
|
|
245
|
-
'data-obj-id': evaluatedSchema.id,
|
|
246
|
-
'data-obj-type': evaluatedSchema.type,
|
|
247
|
-
...props
|
|
248
|
-
})}
|
|
249
|
-
</SchemaErrorBoundary>
|
|
250
|
-
);
|
|
251
|
-
debugTimeEnd(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
|
|
252
|
-
|
|
253
|
-
// Report render perf to DebugCollector when debug mode is active
|
|
254
|
-
if (isDebug && renderStart) {
|
|
255
|
-
const durationMs = performance.now() - renderStart;
|
|
256
|
-
DebugCollector.getInstance().addPerf({
|
|
257
|
-
type: evaluatedSchema.type,
|
|
258
|
-
id: evaluatedSchema.id,
|
|
259
|
-
durationMs,
|
|
260
|
-
timestamp: Date.now(),
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return rendered;
|
|
265
|
-
});
|
|
266
|
-
SchemaRenderer.displayName = 'SchemaRenderer';
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
11
|
-
import React from 'react';
|
|
12
|
-
import { createLazyPlugin, preloadPlugin } from '../LazyPluginLoader';
|
|
13
|
-
|
|
14
|
-
describe('createLazyPlugin', () => {
|
|
15
|
-
it('should create a lazy-loaded component', async () => {
|
|
16
|
-
const TestComponent = () => <div>Test Component</div>;
|
|
17
|
-
|
|
18
|
-
const LazyComponent = createLazyPlugin(
|
|
19
|
-
() => Promise.resolve({ default: TestComponent })
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
render(<LazyComponent />);
|
|
23
|
-
|
|
24
|
-
await waitFor(() => {
|
|
25
|
-
expect(screen.getByText('Test Component')).toBeInTheDocument();
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should show custom fallback while loading', async () => {
|
|
30
|
-
const TestComponent = () => <div>Test Component</div>;
|
|
31
|
-
|
|
32
|
-
const LazyComponent = createLazyPlugin(
|
|
33
|
-
() => new Promise<{ default: React.ComponentType }>((resolve) => {
|
|
34
|
-
setTimeout(() => resolve({ default: TestComponent }), 100);
|
|
35
|
-
}),
|
|
36
|
-
{ fallback: <div>Loading...</div> }
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
render(<LazyComponent />);
|
|
40
|
-
|
|
41
|
-
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
42
|
-
|
|
43
|
-
await waitFor(() => {
|
|
44
|
-
expect(screen.getByText('Test Component')).toBeInTheDocument();
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('should pass props to the lazy component', async () => {
|
|
49
|
-
interface TestProps {
|
|
50
|
-
message: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const TestComponent = ({ message }: TestProps) => <div>{message}</div>;
|
|
54
|
-
|
|
55
|
-
const LazyComponent = createLazyPlugin<TestProps>(
|
|
56
|
-
() => Promise.resolve({ default: TestComponent })
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
render(<LazyComponent message="Hello World" />);
|
|
60
|
-
|
|
61
|
-
await waitFor(() => {
|
|
62
|
-
expect(screen.getByText('Hello World')).toBeInTheDocument();
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should handle loading errors gracefully', async () => {
|
|
67
|
-
// Mock console.error to avoid noise in test output
|
|
68
|
-
const originalError = console.error;
|
|
69
|
-
console.error = vi.fn();
|
|
70
|
-
|
|
71
|
-
const LazyComponent = createLazyPlugin<any>(
|
|
72
|
-
() => Promise.reject(new Error('Load error'))
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
expect(() => render(<LazyComponent />)).not.toThrow();
|
|
76
|
-
|
|
77
|
-
// Restore console.error
|
|
78
|
-
console.error = originalError;
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should preload a plugin without rendering', async () => {
|
|
82
|
-
const TestComponent = () => <div>Preloaded</div>;
|
|
83
|
-
const importFn = vi.fn(() => Promise.resolve({ default: TestComponent }));
|
|
84
|
-
|
|
85
|
-
const result = await preloadPlugin(importFn);
|
|
86
|
-
|
|
87
|
-
expect(importFn).toHaveBeenCalledTimes(1);
|
|
88
|
-
expect(result).toEqual({ default: TestComponent });
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should render errorFallback when component fails to load', async () => {
|
|
92
|
-
const originalError = console.error;
|
|
93
|
-
console.error = vi.fn();
|
|
94
|
-
|
|
95
|
-
const ErrorFallback = ({ error, retry }: { error: Error; retry: () => void }) => (
|
|
96
|
-
<div>
|
|
97
|
-
<span>Error: {error.message}</span>
|
|
98
|
-
<button onClick={retry}>Retry</button>
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const LazyComponent = createLazyPlugin(
|
|
103
|
-
() => Promise.reject(new Error('chunk failed')),
|
|
104
|
-
{ retries: 0, errorFallback: ErrorFallback }
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
render(<LazyComponent />);
|
|
108
|
-
|
|
109
|
-
await waitFor(() => {
|
|
110
|
-
expect(screen.getByText('Error: chunk failed')).toBeInTheDocument();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument();
|
|
114
|
-
|
|
115
|
-
console.error = originalError;
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should not retry when retries=0', async () => {
|
|
119
|
-
const originalError = console.error;
|
|
120
|
-
console.error = vi.fn();
|
|
121
|
-
|
|
122
|
-
const importFn = vi.fn(() => Promise.reject(new Error('fail')));
|
|
123
|
-
|
|
124
|
-
const ErrorFallback = ({ error }: { error: Error; retry: () => void }) => (
|
|
125
|
-
<div>Error: {error.message}</div>
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
const LazyComponent = createLazyPlugin(importFn, {
|
|
129
|
-
retries: 0,
|
|
130
|
-
errorFallback: ErrorFallback,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
render(<LazyComponent />);
|
|
134
|
-
|
|
135
|
-
await waitFor(() => {
|
|
136
|
-
expect(screen.getByText('Error: fail')).toBeInTheDocument();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Called only once — no retries
|
|
140
|
-
expect(importFn).toHaveBeenCalledTimes(1);
|
|
141
|
-
|
|
142
|
-
console.error = originalError;
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('should retry on failure then succeed', async () => {
|
|
146
|
-
const originalError = console.error;
|
|
147
|
-
console.error = vi.fn();
|
|
148
|
-
|
|
149
|
-
const TestComponent = () => <div>Success after retry</div>;
|
|
150
|
-
|
|
151
|
-
let callCount = 0;
|
|
152
|
-
const importFn = vi.fn(() => {
|
|
153
|
-
callCount++;
|
|
154
|
-
if (callCount <= 2) {
|
|
155
|
-
return Promise.reject(new Error('transient error'));
|
|
156
|
-
}
|
|
157
|
-
return Promise.resolve({ default: TestComponent as React.ComponentType });
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const LazyComponent = createLazyPlugin(importFn, {
|
|
161
|
-
retries: 3,
|
|
162
|
-
retryDelay: 10,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
render(<LazyComponent />);
|
|
166
|
-
|
|
167
|
-
await waitFor(() => {
|
|
168
|
-
expect(screen.getByText('Success after retry')).toBeInTheDocument();
|
|
169
|
-
}, { timeout: 2000 });
|
|
170
|
-
|
|
171
|
-
// First call + 2 retries (fails) + 1 success = called 3 times
|
|
172
|
-
expect(importFn).toHaveBeenCalledTimes(3);
|
|
173
|
-
|
|
174
|
-
console.error = originalError;
|
|
175
|
-
});
|
|
176
|
-
});
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
-
import { render, screen } from '@testing-library/react';
|
|
11
|
-
import React from 'react';
|
|
12
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
13
|
-
import { SchemaRenderer } from '../SchemaRenderer';
|
|
14
|
-
|
|
15
|
-
// A simple test component that forwards ARIA attributes
|
|
16
|
-
const TestWidget: React.FC<any> = (props) => (
|
|
17
|
-
<div
|
|
18
|
-
data-testid="test-widget"
|
|
19
|
-
aria-label={props['aria-label']}
|
|
20
|
-
aria-describedby={props['aria-describedby']}
|
|
21
|
-
role={props['role']}
|
|
22
|
-
>
|
|
23
|
-
{props.content || 'Test'}
|
|
24
|
-
</div>
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
describe('SchemaRenderer AriaProps injection', () => {
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
ComponentRegistry.register('test-widget', TestWidget);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
afterEach(() => {
|
|
33
|
-
ComponentRegistry.unregister?.('test-widget');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should inject aria-label from ariaLabel string', () => {
|
|
37
|
-
render(
|
|
38
|
-
<SchemaRenderer
|
|
39
|
-
schema={{
|
|
40
|
-
type: 'test-widget',
|
|
41
|
-
ariaLabel: 'Close dialog',
|
|
42
|
-
}}
|
|
43
|
-
/>
|
|
44
|
-
);
|
|
45
|
-
const el = screen.getByTestId('test-widget');
|
|
46
|
-
expect(el).toHaveAttribute('aria-label', 'Close dialog');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should resolve ariaLabel from I18nLabel object', () => {
|
|
50
|
-
render(
|
|
51
|
-
<SchemaRenderer
|
|
52
|
-
schema={{
|
|
53
|
-
type: 'test-widget',
|
|
54
|
-
ariaLabel: { key: 'dialog.close', defaultValue: 'Close dialog' },
|
|
55
|
-
}}
|
|
56
|
-
/>
|
|
57
|
-
);
|
|
58
|
-
const el = screen.getByTestId('test-widget');
|
|
59
|
-
expect(el).toHaveAttribute('aria-label', 'Close dialog');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should inject aria-describedby from ariaDescribedBy', () => {
|
|
63
|
-
render(
|
|
64
|
-
<SchemaRenderer
|
|
65
|
-
schema={{
|
|
66
|
-
type: 'test-widget',
|
|
67
|
-
ariaDescribedBy: 'help-text-1',
|
|
68
|
-
}}
|
|
69
|
-
/>
|
|
70
|
-
);
|
|
71
|
-
const el = screen.getByTestId('test-widget');
|
|
72
|
-
expect(el).toHaveAttribute('aria-describedby', 'help-text-1');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should inject role from schema', () => {
|
|
76
|
-
render(
|
|
77
|
-
<SchemaRenderer
|
|
78
|
-
schema={{
|
|
79
|
-
type: 'test-widget',
|
|
80
|
-
role: 'navigation',
|
|
81
|
-
}}
|
|
82
|
-
/>
|
|
83
|
-
);
|
|
84
|
-
const el = screen.getByTestId('test-widget');
|
|
85
|
-
expect(el).toHaveAttribute('role', 'navigation');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should inject all ARIA props together', () => {
|
|
89
|
-
render(
|
|
90
|
-
<SchemaRenderer
|
|
91
|
-
schema={{
|
|
92
|
-
type: 'test-widget',
|
|
93
|
-
ariaLabel: 'Main nav',
|
|
94
|
-
ariaDescribedBy: 'nav-desc',
|
|
95
|
-
role: 'navigation',
|
|
96
|
-
}}
|
|
97
|
-
/>
|
|
98
|
-
);
|
|
99
|
-
const el = screen.getByTestId('test-widget');
|
|
100
|
-
expect(el).toHaveAttribute('aria-label', 'Main nav');
|
|
101
|
-
expect(el).toHaveAttribute('aria-describedby', 'nav-desc');
|
|
102
|
-
expect(el).toHaveAttribute('role', 'navigation');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should not inject ARIA attrs when not in schema', () => {
|
|
106
|
-
render(
|
|
107
|
-
<SchemaRenderer
|
|
108
|
-
schema={{
|
|
109
|
-
type: 'test-widget',
|
|
110
|
-
content: 'Hello',
|
|
111
|
-
}}
|
|
112
|
-
/>
|
|
113
|
-
);
|
|
114
|
-
const el = screen.getByTestId('test-widget');
|
|
115
|
-
expect(el).not.toHaveAttribute('aria-label');
|
|
116
|
-
expect(el).not.toHaveAttribute('aria-describedby');
|
|
117
|
-
});
|
|
118
|
-
});
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
-
import { render } from '@testing-library/react';
|
|
11
|
-
import React from 'react';
|
|
12
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
13
|
-
import { SchemaRenderer } from '../SchemaRenderer';
|
|
14
|
-
import { SchemaRendererProvider } from '../context/SchemaRendererContext';
|
|
15
|
-
|
|
16
|
-
// Suppress console.warn from deprecated namespace registration
|
|
17
|
-
const originalWarn = console.warn;
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
console.warn = vi.fn();
|
|
20
|
-
});
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
console.warn = originalWarn;
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// A simple test component that passes all props through to a div
|
|
26
|
-
const TestDiv: React.FC<any> = (props) => {
|
|
27
|
-
const { schema, ...rest } = props;
|
|
28
|
-
return <div data-testid="test-div" {...rest} />;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
describe('SchemaRenderer debug attributes', () => {
|
|
32
|
-
beforeEach(() => {
|
|
33
|
-
ComponentRegistry.register('test-debug-div', TestDiv);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should NOT inject data-debug-* attributes when debug is off', () => {
|
|
37
|
-
const { getByTestId } = render(
|
|
38
|
-
<SchemaRendererProvider dataSource={{}}>
|
|
39
|
-
<SchemaRenderer schema={{ type: 'test-debug-div', id: 'myBtn' }} />
|
|
40
|
-
</SchemaRendererProvider>,
|
|
41
|
-
);
|
|
42
|
-
const el = getByTestId('test-div');
|
|
43
|
-
expect(el.getAttribute('data-debug-type')).toBeNull();
|
|
44
|
-
expect(el.getAttribute('data-debug-id')).toBeNull();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should inject data-debug-type when debug is enabled', () => {
|
|
48
|
-
const { getByTestId } = render(
|
|
49
|
-
<SchemaRendererProvider dataSource={{}} debug={true}>
|
|
50
|
-
<SchemaRenderer schema={{ type: 'test-debug-div', id: 'myBtn' }} />
|
|
51
|
-
</SchemaRendererProvider>,
|
|
52
|
-
);
|
|
53
|
-
const el = getByTestId('test-div');
|
|
54
|
-
expect(el.getAttribute('data-debug-type')).toBe('test-debug-div');
|
|
55
|
-
expect(el.getAttribute('data-debug-id')).toBe('myBtn');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should inject data-debug-type when debugFlags.enabled is true', () => {
|
|
59
|
-
const { getByTestId } = render(
|
|
60
|
-
<SchemaRendererProvider dataSource={{}} debugFlags={{ enabled: true }}>
|
|
61
|
-
<SchemaRenderer schema={{ type: 'test-debug-div' }} />
|
|
62
|
-
</SchemaRendererProvider>,
|
|
63
|
-
);
|
|
64
|
-
const el = getByTestId('test-div');
|
|
65
|
-
expect(el.getAttribute('data-debug-type')).toBe('test-debug-div');
|
|
66
|
-
// No id on schema, so data-debug-id should not be present
|
|
67
|
-
expect(el.getAttribute('data-debug-id')).toBeNull();
|
|
68
|
-
});
|
|
69
|
-
});
|