Skip to content

Commit d5e9ae7

Browse files
committed
Merge branch 'main' into copilot/evaluate-project-architecture
2 parents 45687de + f7fc034 commit d5e9ae7

File tree

3 files changed

+297
-3
lines changed

3 files changed

+297
-3
lines changed

packages/components/src/renderers/form/input.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const InputRenderer = ({ schema, className, onChange, value, ...props }: { schem
6363
};
6464

6565
ComponentRegistry.register('input', InputRenderer, {
66+
namespace: 'ui',
6667
label: 'Input Field',
6768
inputs: [
6869
{ name: 'label', type: 'string', label: 'Label' },
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
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 { Registry } from '../Registry';
11+
12+
describe('Registry', () => {
13+
let registry: Registry;
14+
let consoleWarnSpy: any;
15+
16+
beforeEach(() => {
17+
registry = new Registry();
18+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
19+
});
20+
21+
afterEach(() => {
22+
consoleWarnSpy.mockRestore();
23+
});
24+
25+
describe('Basic Registration', () => {
26+
it('should register a component without namespace', () => {
27+
const component = () => 'test';
28+
registry.register('button', component);
29+
30+
expect(registry.has('button')).toBe(true);
31+
expect(registry.get('button')).toBe(component);
32+
});
33+
34+
it('should warn when registering without namespace', () => {
35+
const component = () => 'test';
36+
registry.register('button', component);
37+
38+
expect(consoleWarnSpy).toHaveBeenCalledWith(
39+
expect.stringContaining('Registering component "button" without a namespace is deprecated')
40+
);
41+
});
42+
43+
it('should register a component with namespace', () => {
44+
const component = () => 'test';
45+
registry.register('button', component, { namespace: 'ui' });
46+
47+
expect(registry.has('button', 'ui')).toBe(true);
48+
expect(registry.get('button', 'ui')).toBe(component);
49+
});
50+
51+
it('should not warn when registering with namespace', () => {
52+
const component = () => 'test';
53+
registry.register('button', component, { namespace: 'ui' });
54+
55+
expect(consoleWarnSpy).not.toHaveBeenCalled();
56+
});
57+
});
58+
59+
describe('Namespaced Registration', () => {
60+
it('should register components with the same name in different namespaces', () => {
61+
const gridComponent1 = () => 'grid1';
62+
const gridComponent2 = () => 'grid2';
63+
64+
registry.register('grid', gridComponent1, { namespace: 'plugin-grid' });
65+
registry.register('grid', gridComponent2, { namespace: 'plugin-aggrid' });
66+
67+
expect(registry.get('grid', 'plugin-grid')).toBe(gridComponent1);
68+
expect(registry.get('grid', 'plugin-aggrid')).toBe(gridComponent2);
69+
});
70+
71+
it('should store full type as namespace:type', () => {
72+
const component = () => 'test';
73+
registry.register('button', component, { namespace: 'ui' });
74+
75+
const config = registry.getConfig('button', 'ui');
76+
expect(config?.type).toBe('ui:button');
77+
});
78+
79+
it('should preserve namespace in component config', () => {
80+
const component = () => 'test';
81+
registry.register('button', component, {
82+
namespace: 'ui',
83+
label: 'Button',
84+
category: 'form'
85+
});
86+
87+
const config = registry.getConfig('button', 'ui');
88+
expect(config?.namespace).toBe('ui');
89+
expect(config?.label).toBe('Button');
90+
expect(config?.category).toBe('form');
91+
});
92+
});
93+
94+
describe('Namespace Lookup with Fallback', () => {
95+
it('should not fallback when namespace is explicitly specified', () => {
96+
const component = () => 'test';
97+
registry.register('button', component);
98+
99+
// When no namespace is specified, should find it
100+
expect(registry.get('button')).toBe(component);
101+
102+
// When namespace is specified but component isn't in that namespace, should return undefined
103+
expect(registry.get('button', 'ui')).toBeUndefined();
104+
});
105+
106+
it('should prefer namespaced component over non-namespaced', () => {
107+
const component1 = () => 'non-namespaced';
108+
const component2 = () => 'namespaced';
109+
110+
registry.register('button', component1);
111+
registry.register('button', component2, { namespace: 'ui' });
112+
113+
// When searching with namespace, should get namespaced version
114+
expect(registry.get('button', 'ui')).toBe(component2);
115+
116+
// When searching without namespace, should get the latest registered (namespaced one due to backward compatibility)
117+
expect(registry.get('button')).toBe(component2);
118+
});
119+
120+
it('should return undefined when component not found in any namespace', () => {
121+
expect(registry.get('nonexistent', 'ui')).toBeUndefined();
122+
expect(registry.get('nonexistent')).toBeUndefined();
123+
});
124+
});
125+
126+
describe('has() method', () => {
127+
it('should check existence with namespace', () => {
128+
const component = () => 'test';
129+
registry.register('button', component, { namespace: 'ui' });
130+
131+
expect(registry.has('button', 'ui')).toBe(true);
132+
// Due to backward compatibility, non-namespaced lookup also works
133+
expect(registry.has('button')).toBe(true);
134+
// Other namespaces should return false
135+
expect(registry.has('button', 'other')).toBe(false);
136+
});
137+
138+
it('should fallback to non-namespaced check only when no namespace provided', () => {
139+
const component = () => 'test';
140+
registry.register('button', component);
141+
142+
expect(registry.has('button')).toBe(true);
143+
// When namespace is explicitly requested, should not find non-namespaced component
144+
expect(registry.has('button', 'ui')).toBe(false);
145+
});
146+
});
147+
148+
describe('getConfig() method', () => {
149+
it('should get config with namespace', () => {
150+
const component = () => 'test';
151+
registry.register('button', component, {
152+
namespace: 'ui',
153+
label: 'Button'
154+
});
155+
156+
const config = registry.getConfig('button', 'ui');
157+
expect(config).toBeDefined();
158+
expect(config?.component).toBe(component);
159+
expect(config?.label).toBe('Button');
160+
});
161+
162+
it('should not fallback when namespace is explicitly provided', () => {
163+
const component = () => 'test';
164+
registry.register('button', component, { label: 'Button' });
165+
166+
// When no namespace is provided, should find it
167+
const config1 = registry.getConfig('button');
168+
expect(config1).toBeDefined();
169+
170+
// When namespace is provided but component isn't in that namespace, should return undefined
171+
const config2 = registry.getConfig('button', 'ui');
172+
expect(config2).toBeUndefined();
173+
});
174+
});
175+
176+
describe('getAllTypes() and getAllConfigs()', () => {
177+
it('should return all registered types including namespaced ones', () => {
178+
registry.register('button', () => 'b1');
179+
registry.register('input', () => 'i1', { namespace: 'ui' });
180+
registry.register('grid', () => 'g1', { namespace: 'plugin-grid' });
181+
182+
const types = registry.getAllTypes();
183+
// Due to backward compatibility, namespaced components are stored under both keys
184+
expect(types).toContain('button');
185+
expect(types).toContain('ui:input');
186+
expect(types).toContain('input'); // backward compat
187+
expect(types).toContain('plugin-grid:grid');
188+
expect(types).toContain('grid'); // backward compat
189+
});
190+
191+
it('should return all configs', () => {
192+
registry.register('button', () => 'b1', { label: 'Button' });
193+
registry.register('input', () => 'i1', {
194+
namespace: 'ui',
195+
label: 'Input'
196+
});
197+
198+
const configs = registry.getAllConfigs();
199+
// Due to backward compatibility, namespaced components are stored twice
200+
expect(configs.length).toBeGreaterThanOrEqual(2);
201+
expect(configs.map(c => c.type)).toContain('button');
202+
expect(configs.map(c => c.type)).toContain('ui:input');
203+
});
204+
});
205+
206+
describe('Conflict Prevention', () => {
207+
it('should allow same type name in different namespaces', () => {
208+
const grid1 = () => 'grid-plugin-1';
209+
const grid2 = () => 'grid-plugin-2';
210+
const grid3 = () => 'aggrid-plugin';
211+
212+
registry.register('grid', grid1, { namespace: 'plugin-grid' });
213+
registry.register('grid', grid2, { namespace: 'plugin-view' });
214+
registry.register('grid', grid3, { namespace: 'plugin-aggrid' });
215+
216+
expect(registry.get('grid', 'plugin-grid')).toBe(grid1);
217+
expect(registry.get('grid', 'plugin-view')).toBe(grid2);
218+
expect(registry.get('grid', 'plugin-aggrid')).toBe(grid3);
219+
});
220+
221+
it('should handle complex namespace names', () => {
222+
const component = () => 'test';
223+
registry.register('table', component, { namespace: 'plugin-advanced-grid' });
224+
225+
expect(registry.get('table', 'plugin-advanced-grid')).toBe(component);
226+
expect(registry.getConfig('table', 'plugin-advanced-grid')?.type).toBe('plugin-advanced-grid:table');
227+
});
228+
});
229+
230+
describe('Backward Compatibility', () => {
231+
it('should maintain compatibility with existing non-namespaced code', () => {
232+
const component = () => 'test';
233+
234+
// Old-style registration
235+
registry.register('button', component);
236+
237+
// Old-style retrieval should still work
238+
expect(registry.get('button')).toBe(component);
239+
expect(registry.has('button')).toBe(true);
240+
expect(registry.getConfig('button')).toBeDefined();
241+
});
242+
243+
it('should support mixed namespaced and non-namespaced registrations', () => {
244+
const oldButton = () => 'old';
245+
const newButton = () => 'new';
246+
247+
registry.register('button-old', oldButton);
248+
registry.register('button-new', newButton, { namespace: 'ui' });
249+
250+
expect(registry.get('button-old')).toBe(oldButton);
251+
expect(registry.get('button-new', 'ui')).toBe(newButton);
252+
});
253+
254+
it('should allow non-namespaced lookup of namespaced components', () => {
255+
const component = () => 'test';
256+
257+
// Register with namespace
258+
registry.register('button', component, { namespace: 'ui' });
259+
260+
// Should be findable both ways for backward compatibility
261+
expect(registry.get('button')).toBe(component);
262+
expect(registry.get('button', 'ui')).toBe(component);
263+
264+
// The full type should be namespaced
265+
const config = registry.getConfig('button');
266+
expect(config?.type).toBe('ui:button');
267+
});
268+
});
269+
270+
describe('Edge Cases', () => {
271+
it('should handle empty namespace string', () => {
272+
const component = () => 'test';
273+
registry.register('button', component, { namespace: '' });
274+
275+
// Empty namespace should be treated as no namespace
276+
expect(registry.get('button')).toBe(component);
277+
});
278+
279+
it('should handle namespace with special characters', () => {
280+
const component = () => 'test';
281+
registry.register('button', component, { namespace: 'plugin-my-custom' });
282+
283+
expect(registry.get('button', 'plugin-my-custom')).toBe(component);
284+
});
285+
286+
it('should handle undefined meta', () => {
287+
const component = () => 'test';
288+
registry.register('button', component, undefined);
289+
290+
expect(registry.get('button')).toBe(component);
291+
});
292+
});
293+
});

packages/data-objectstack/src/cache/MetadataCache.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,12 @@ describe('MetadataCache', () => {
101101
await new Promise(resolve => setTimeout(resolve, 100));
102102
await cache.get('test', fetcher);
103103

104-
// Access again after another 100ms (total 200ms from first, but 100ms from last access)
105-
await new Promise(resolve => setTimeout(resolve, 100));
104+
// Access again after another 110ms (total 210ms from first, ensuring TTL has passed)
105+
await new Promise(resolve => setTimeout(resolve, 110));
106106

107107
// Should still be in cache because we're checking timestamp, not last accessed
108108
// Actually, the implementation uses timestamp for expiration, not lastAccessed
109-
// So after 200ms total, it should expire
109+
// So after 210ms total (> 200ms TTL), it should expire
110110
await cache.get('test', fetcher);
111111

112112
// Should have been called twice - initial + after expiration

0 commit comments

Comments
 (0)