@blac/core 2.0.0-rc-11 → 2.0.0-rc-14

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.
Files changed (55) hide show
  1. package/dist/index.cjs.js +2 -2
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.d.ts +739 -260
  4. package/dist/index.es.js +1924 -1237
  5. package/dist/index.es.js.map +1 -1
  6. package/package.json +6 -7
  7. package/src/Blac.ts +82 -69
  8. package/src/Bloc.ts +45 -3
  9. package/src/BlocBase.ts +263 -454
  10. package/src/Cubit.ts +1 -16
  11. package/src/__tests__/Blac.config.test.ts +10 -17
  12. package/src/__tests__/Bloc.event.test.ts +27 -52
  13. package/src/__tests__/BlocBase.subscription.test.ts +349 -0
  14. package/src/__tests__/Cubit.test.ts +39 -50
  15. package/src/__tests__/memory-leaks.test.ts +383 -0
  16. package/src/__tests__/plugins.test.ts +464 -0
  17. package/src/adapter/BlacAdapter.ts +347 -207
  18. package/src/adapter/ProxyFactory.ts +196 -255
  19. package/src/adapter/__tests__/BlacAdapter.test.ts +71 -77
  20. package/src/adapter/__tests__/ProxyFactory.test.ts +1 -1
  21. package/src/adapter/index.ts +0 -1
  22. package/src/errors/BlacError.ts +69 -0
  23. package/src/errors/ErrorManager.ts +101 -0
  24. package/src/errors/handleError.ts +114 -0
  25. package/src/errors/index.ts +13 -0
  26. package/src/events.ts +7 -1
  27. package/src/index.ts +10 -1
  28. package/src/lifecycle/BlocLifecycle.ts +156 -0
  29. package/src/lifecycle/index.ts +1 -0
  30. package/src/plugins/BlocPluginRegistry.ts +240 -0
  31. package/src/plugins/SystemPluginRegistry.ts +242 -0
  32. package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
  33. package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
  34. package/src/plugins/index.ts +8 -0
  35. package/src/plugins/types.ts +129 -0
  36. package/src/subscription/README.md +132 -0
  37. package/src/subscription/SubscriptionManager.ts +395 -0
  38. package/src/subscription/__tests__/SubscriptionManager.test.ts +355 -0
  39. package/src/subscription/index.ts +2 -0
  40. package/src/subscription/types.ts +90 -0
  41. package/src/testing.ts +31 -37
  42. package/src/types.ts +4 -21
  43. package/src/utils/BatchingManager.ts +59 -0
  44. package/src/utils/RerenderLogger.ts +265 -0
  45. package/src/utils/__tests__/generateInstanceId.test.ts +104 -0
  46. package/src/utils/__tests__/shallowEqual.test.ts +171 -0
  47. package/src/utils/__tests__/uuid.test.ts +144 -0
  48. package/src/utils/generateInstanceId.ts +54 -0
  49. package/src/utils/shallowEqual.ts +1 -1
  50. package/README.md +0 -329
  51. package/src/BlacObserver.ts +0 -186
  52. package/src/__tests__/BlocBase.lifecycle.test.ts +0 -441
  53. package/src/__tests__/props.test.ts +0 -256
  54. package/src/adapter/ConsumerTracker.ts +0 -241
  55. package/src/adapter/__tests__/ConsumerTracker.test.ts +0 -562
@@ -0,0 +1,265 @@
1
+ import { Blac } from '../Blac';
2
+ import { BlocBase } from '../BlocBase';
3
+
4
+ export interface RerenderInfo {
5
+ componentName: string;
6
+ blocName: string;
7
+ blocId: string;
8
+ renderCount: number;
9
+ reason: RerenderReason;
10
+ timestamp: number;
11
+ duration?: number;
12
+ stackTrace?: string;
13
+ }
14
+
15
+ export interface RerenderReason {
16
+ type:
17
+ | 'state-change'
18
+ | 'dependency-change'
19
+ | 'mount'
20
+ | 'props-change'
21
+ | 'unknown';
22
+ changedPaths?: string[];
23
+ oldValues?: Record<string, any>;
24
+ newValues?: Record<string, any>;
25
+ dependencies?: string[];
26
+ description?: string;
27
+ }
28
+
29
+ /**
30
+ * Utility class for logging component rerenders with detailed debugging information
31
+ */
32
+ export class RerenderLogger {
33
+ private static componentRenderCounts = new Map<string, number>();
34
+ private static lastRenderTimestamps = new Map<string, number>();
35
+ private static groupedLogs = new Map<string, RerenderInfo[]>();
36
+ private static groupTimeouts = new Map<string, NodeJS.Timeout>();
37
+
38
+ /**
39
+ * Log a component rerender with detailed information
40
+ */
41
+ static logRerender(info: RerenderInfo): void {
42
+ // RerenderLogger is now controlled by the RenderLoggingPlugin
43
+ // This method is called by the plugin which already handles configuration
44
+ const config = (Blac as any)._config?.rerenderLogging;
45
+ if (!config) return;
46
+
47
+ // Parse config
48
+ const loggingConfig = this.parseConfig(config);
49
+ if (!loggingConfig.enabled) return;
50
+
51
+ // Apply filter if provided
52
+ if (
53
+ loggingConfig.filter &&
54
+ !loggingConfig.filter({
55
+ componentName: info.componentName,
56
+ blocName: info.blocName,
57
+ })
58
+ ) {
59
+ return;
60
+ }
61
+
62
+ // Track render count
63
+ const key = `${info.componentName}-${info.blocId}`;
64
+ const currentCount = this.componentRenderCounts.get(key) || 0;
65
+ this.componentRenderCounts.set(key, currentCount + 1);
66
+ info.renderCount = currentCount + 1;
67
+
68
+ // Calculate duration if possible
69
+ const lastTimestamp = this.lastRenderTimestamps.get(key);
70
+ if (lastTimestamp) {
71
+ info.duration = info.timestamp - lastTimestamp;
72
+ }
73
+ this.lastRenderTimestamps.set(key, info.timestamp);
74
+
75
+ // Add stack trace if requested
76
+ if (loggingConfig.includeStackTrace && loggingConfig.level === 'detailed') {
77
+ info.stackTrace = new Error().stack;
78
+ }
79
+
80
+ // Handle grouping
81
+ if (loggingConfig.groupRerenders) {
82
+ this.logGrouped(info, loggingConfig);
83
+ } else {
84
+ this.logImmediate(info, loggingConfig);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Parse the logging configuration
90
+ */
91
+ private static parseConfig(config: any) {
92
+ if (config === true) {
93
+ return { enabled: true, level: 'normal' as const };
94
+ }
95
+ if (typeof config === 'string') {
96
+ return {
97
+ enabled: true,
98
+ level:
99
+ config === 'minimal' ? ('minimal' as const) : ('detailed' as const),
100
+ };
101
+ }
102
+ return {
103
+ enabled: config.enabled,
104
+ level: config.level || ('normal' as const),
105
+ filter: config.filter,
106
+ includeStackTrace: config.includeStackTrace,
107
+ groupRerenders: config.groupRerenders,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Log immediately without grouping
113
+ */
114
+ private static logImmediate(
115
+ info: RerenderInfo,
116
+ config: ReturnType<typeof RerenderLogger.parseConfig>,
117
+ ): void {
118
+ const message = this.formatMessage(info, config.level!);
119
+
120
+ if (config.level === 'detailed') {
121
+ console.group(message.header);
122
+ if (message.details) console.log(message.details);
123
+ if (message.values) console.table(message.values);
124
+ if (info.stackTrace) console.log('Stack trace:', info.stackTrace);
125
+ console.groupEnd();
126
+ } else {
127
+ console.log(message.header, message.details || '');
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Log with grouping (batch multiple rerenders)
133
+ */
134
+ private static logGrouped(
135
+ info: RerenderInfo,
136
+ config: ReturnType<typeof RerenderLogger.parseConfig>,
137
+ ): void {
138
+ const groupKey = `${info.componentName}-${info.blocId}`;
139
+
140
+ // Clear existing timeout
141
+ const existingTimeout = this.groupTimeouts.get(groupKey);
142
+ if (existingTimeout) {
143
+ clearTimeout(existingTimeout);
144
+ }
145
+
146
+ // Add to group
147
+ const group = this.groupedLogs.get(groupKey) || [];
148
+ group.push(info);
149
+ this.groupedLogs.set(groupKey, group);
150
+
151
+ // Set new timeout
152
+ const timeout = setTimeout(() => {
153
+ const logs = this.groupedLogs.get(groupKey) || [];
154
+ this.groupedLogs.delete(groupKey);
155
+ this.groupTimeouts.delete(groupKey);
156
+
157
+ if (logs.length === 1) {
158
+ this.logImmediate(logs[0], config);
159
+ } else {
160
+ console.group(
161
+ `🔄 ${info.componentName} rendered ${logs.length} times (${info.blocName})`,
162
+ );
163
+ logs.forEach((log, index) => {
164
+ const message = this.formatMessage(log, config.level!);
165
+ console.log(` ${index + 1}. ${message.details || message.header}`);
166
+ });
167
+ console.groupEnd();
168
+ }
169
+ }, 50); // 50ms debounce
170
+
171
+ this.groupTimeouts.set(groupKey, timeout);
172
+ }
173
+
174
+ /**
175
+ * Format the log message based on level
176
+ */
177
+ private static formatMessage(
178
+ info: RerenderInfo,
179
+ level: 'minimal' | 'normal' | 'detailed',
180
+ ) {
181
+ const emoji = this.getReasonEmoji(info.reason.type);
182
+
183
+ if (level === 'minimal') {
184
+ return {
185
+ header: `${emoji} ${info.componentName} #${info.renderCount}`,
186
+ details: null,
187
+ values: null,
188
+ };
189
+ }
190
+
191
+ const header = `${emoji} ${info.componentName} rerender #${info.renderCount} (${info.blocName})`;
192
+
193
+ let details = `Reason: ${info.reason.description || info.reason.type}`;
194
+
195
+ if (info.duration !== undefined) {
196
+ details += ` | Time since last: ${info.duration}ms`;
197
+ }
198
+
199
+ if (
200
+ level === 'normal' &&
201
+ info.reason.changedPaths &&
202
+ info.reason.changedPaths.length > 0
203
+ ) {
204
+ details += ` | Changed: ${info.reason.changedPaths.join(', ')}`;
205
+ }
206
+
207
+ let values: Record<string, { old: any; new: any }> | null = null;
208
+ if (
209
+ level === 'detailed' &&
210
+ info.reason.changedPaths &&
211
+ info.reason.changedPaths.length > 0
212
+ ) {
213
+ values = {};
214
+ info.reason.changedPaths.forEach((path) => {
215
+ values![path] = {
216
+ old: info.reason.oldValues?.[path],
217
+ new: info.reason.newValues?.[path],
218
+ };
219
+ });
220
+
221
+ // If we have values to show in detailed mode, ensure they're not empty
222
+ if (Object.keys(values).length === 0) {
223
+ values = null;
224
+ }
225
+ }
226
+
227
+ return { header, details, values };
228
+ }
229
+
230
+ /**
231
+ * Get emoji for reason type
232
+ */
233
+ private static getReasonEmoji(type: RerenderReason['type']): string {
234
+ switch (type) {
235
+ case 'state-change':
236
+ return '📊';
237
+ case 'dependency-change':
238
+ return '🔗';
239
+ case 'mount':
240
+ return '🚀';
241
+ case 'props-change':
242
+ return '📦';
243
+ default:
244
+ return '🔄';
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Clear all tracking data
250
+ */
251
+ static clear(): void {
252
+ this.componentRenderCounts.clear();
253
+ this.lastRenderTimestamps.clear();
254
+ this.groupedLogs.clear();
255
+ this.groupTimeouts.forEach((timeout) => clearTimeout(timeout));
256
+ this.groupTimeouts.clear();
257
+ }
258
+
259
+ /**
260
+ * Get render count for a component
261
+ */
262
+ static getRenderCount(componentName: string, blocId: string): number {
263
+ return this.componentRenderCounts.get(`${componentName}-${blocId}`) || 0;
264
+ }
265
+ }
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateInstanceIdFromProps } from '../generateInstanceId';
3
+
4
+ describe('generateInstanceIdFromProps', () => {
5
+ it('returns undefined for non-object props', () => {
6
+ expect(generateInstanceIdFromProps(null)).toBeUndefined();
7
+ expect(generateInstanceIdFromProps(undefined)).toBeUndefined();
8
+ expect(generateInstanceIdFromProps('string')).toBeUndefined();
9
+ expect(generateInstanceIdFromProps(123)).toBeUndefined();
10
+ expect(generateInstanceIdFromProps(true)).toBeUndefined();
11
+ });
12
+
13
+ it('returns undefined for objects with no primitive values', () => {
14
+ expect(generateInstanceIdFromProps({})).toBeUndefined();
15
+ expect(
16
+ generateInstanceIdFromProps({ obj: {}, arr: [], fn: () => {} }),
17
+ ).toBeUndefined();
18
+ expect(
19
+ generateInstanceIdFromProps({ date: new Date(), regex: /test/ }),
20
+ ).toBeUndefined();
21
+ });
22
+
23
+ it('generates ID from string values', () => {
24
+ expect(generateInstanceIdFromProps({ id: 'user123' })).toBe('id:user123');
25
+ expect(generateInstanceIdFromProps({ name: 'John', role: 'admin' })).toBe(
26
+ 'name:John|role:admin',
27
+ );
28
+ });
29
+
30
+ it('generates ID from number values', () => {
31
+ expect(generateInstanceIdFromProps({ page: 1 })).toBe('page:1');
32
+ expect(generateInstanceIdFromProps({ x: 10, y: 20 })).toBe('x:10|y:20');
33
+ expect(generateInstanceIdFromProps({ float: 3.14 })).toBe('float:3.14');
34
+ });
35
+
36
+ it('generates ID from boolean values', () => {
37
+ expect(generateInstanceIdFromProps({ active: true })).toBe('active:true');
38
+ expect(generateInstanceIdFromProps({ visible: false })).toBe(
39
+ 'visible:false',
40
+ );
41
+ });
42
+
43
+ it('generates ID from null and undefined values', () => {
44
+ expect(generateInstanceIdFromProps({ val: null })).toBe('val:null');
45
+ expect(generateInstanceIdFromProps({ val: undefined })).toBe(
46
+ 'val:undefined',
47
+ );
48
+ });
49
+
50
+ it('generates ID from mixed primitive types', () => {
51
+ const props = {
52
+ id: 'abc',
53
+ page: 2,
54
+ active: true,
55
+ extra: null,
56
+ missing: undefined,
57
+ };
58
+ expect(generateInstanceIdFromProps(props)).toBe(
59
+ 'active:true|extra:null|id:abc|missing:undefined|page:2',
60
+ );
61
+ });
62
+
63
+ it('ignores non-primitive values', () => {
64
+ const props = {
65
+ id: 'test',
66
+ obj: { nested: true },
67
+ arr: [1, 2, 3],
68
+ fn: () => {},
69
+ page: 5,
70
+ };
71
+ expect(generateInstanceIdFromProps(props)).toBe('id:test|page:5');
72
+ });
73
+
74
+ it('sorts keys alphabetically for deterministic output', () => {
75
+ const props1 = { z: 1, a: 2, m: 3 };
76
+ const props2 = { m: 3, z: 1, a: 2 };
77
+ const props3 = { a: 2, m: 3, z: 1 };
78
+
79
+ const expected = 'a:2|m:3|z:1';
80
+ expect(generateInstanceIdFromProps(props1)).toBe(expected);
81
+ expect(generateInstanceIdFromProps(props2)).toBe(expected);
82
+ expect(generateInstanceIdFromProps(props3)).toBe(expected);
83
+ });
84
+
85
+ it('handles edge cases', () => {
86
+ expect(generateInstanceIdFromProps({ '': 'empty' })).toBe(':empty');
87
+ expect(generateInstanceIdFromProps({ 'key|with|pipes': 'value' })).toBe(
88
+ 'key|with|pipes:value',
89
+ );
90
+ expect(generateInstanceIdFromProps({ 'key:with:colons': 'value' })).toBe(
91
+ 'key:with:colons:value',
92
+ );
93
+ });
94
+
95
+ it('handles special number values', () => {
96
+ expect(generateInstanceIdFromProps({ inf: Infinity })).toBe('inf:Infinity');
97
+ expect(generateInstanceIdFromProps({ ninf: -Infinity })).toBe(
98
+ 'ninf:-Infinity',
99
+ );
100
+ expect(generateInstanceIdFromProps({ nan: NaN })).toBe('nan:NaN');
101
+ expect(generateInstanceIdFromProps({ zero: 0 })).toBe('zero:0');
102
+ expect(generateInstanceIdFromProps({ negZero: -0 })).toBe('negZero:0');
103
+ });
104
+ });
@@ -0,0 +1,171 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { shallowEqual } from '../shallowEqual';
3
+
4
+ describe('shallowEqual', () => {
5
+ describe('Primitive values', () => {
6
+ it('should return true for identical primitives', () => {
7
+ expect(shallowEqual(1, 1)).toBe(true);
8
+ expect(shallowEqual('hello', 'hello')).toBe(true);
9
+ expect(shallowEqual(true, true)).toBe(true);
10
+ expect(shallowEqual(null, null)).toBe(true);
11
+ expect(shallowEqual(undefined, undefined)).toBe(true);
12
+ });
13
+
14
+ it('should return false for different primitives', () => {
15
+ expect(shallowEqual(1, 2)).toBe(false);
16
+ expect(shallowEqual('hello', 'world')).toBe(false);
17
+ expect(shallowEqual(true, false)).toBe(false);
18
+ expect(shallowEqual(null, undefined)).toBe(false);
19
+ expect(shallowEqual(0, false)).toBe(false);
20
+ });
21
+
22
+ it('should handle special numeric values using Object.is', () => {
23
+ expect(shallowEqual(NaN, NaN)).toBe(true); // Object.is(NaN, NaN) is true
24
+ expect(shallowEqual(0, -0)).toBe(false); // Object.is can distinguish +0 and -0
25
+ expect(shallowEqual(-0, -0)).toBe(true);
26
+ expect(shallowEqual(Infinity, Infinity)).toBe(true);
27
+ expect(shallowEqual(-Infinity, -Infinity)).toBe(true);
28
+ expect(shallowEqual(Infinity, -Infinity)).toBe(false);
29
+ });
30
+ });
31
+
32
+ describe('Object comparison', () => {
33
+ it('should return true for objects with same properties', () => {
34
+ expect(shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true);
35
+ expect(
36
+ shallowEqual({ name: 'John', age: 30 }, { name: 'John', age: 30 }),
37
+ ).toBe(true);
38
+ expect(shallowEqual({}, {})).toBe(true);
39
+ });
40
+
41
+ it('should return false for objects with different values', () => {
42
+ expect(shallowEqual({ a: 1 }, { a: 2 })).toBe(false);
43
+ expect(shallowEqual({ name: 'John' }, { name: 'Jane' })).toBe(false);
44
+ });
45
+
46
+ it('should return false for objects with different keys', () => {
47
+ expect(shallowEqual({ a: 1 }, { b: 1 })).toBe(false);
48
+ expect(shallowEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false);
49
+ expect(shallowEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
50
+ });
51
+
52
+ it('should return true for same object reference', () => {
53
+ const obj = { a: 1, b: 2 };
54
+ expect(shallowEqual(obj, obj)).toBe(true);
55
+ });
56
+
57
+ it('should only compare one level deep (shallow)', () => {
58
+ const obj1 = { a: 1, b: { c: 2 } };
59
+ const obj2 = { a: 1, b: { c: 2 } };
60
+ expect(shallowEqual(obj1, obj2)).toBe(false); // Different object references for b
61
+
62
+ const sharedNested = { c: 2 };
63
+ const obj3 = { a: 1, b: sharedNested };
64
+ const obj4 = { a: 1, b: sharedNested };
65
+ expect(shallowEqual(obj3, obj4)).toBe(true); // Same reference for b
66
+ });
67
+
68
+ it('should handle objects with null/undefined values', () => {
69
+ expect(shallowEqual({ a: null }, { a: null })).toBe(true);
70
+ expect(shallowEqual({ a: undefined }, { a: undefined })).toBe(true);
71
+ expect(shallowEqual({ a: null }, { a: undefined })).toBe(false);
72
+ });
73
+ });
74
+
75
+ describe('Array comparison', () => {
76
+ it('should compare arrays shallowly', () => {
77
+ expect(shallowEqual([1, 2, 3], [1, 2, 3])).toBe(true);
78
+ expect(shallowEqual([1, 2, 3], [1, 2, 4])).toBe(false);
79
+ expect(shallowEqual([1, 2], [1, 2, 3])).toBe(false);
80
+ expect(shallowEqual([], [])).toBe(true);
81
+ });
82
+
83
+ it('should return true for same array reference', () => {
84
+ const arr = [1, 2, 3];
85
+ expect(shallowEqual(arr, arr)).toBe(true);
86
+ });
87
+
88
+ it('should handle nested arrays shallowly', () => {
89
+ const nested = [4, 5];
90
+ expect(shallowEqual([1, [2, 3]], [1, [2, 3]])).toBe(false); // Different array references
91
+ expect(shallowEqual([1, nested], [1, nested])).toBe(true); // Same reference
92
+ });
93
+ });
94
+
95
+ describe('Mixed types', () => {
96
+ it('should return false when comparing different types', () => {
97
+ // Arrays and objects with same keys will be considered equal by shallowEqual
98
+ // since it only checks enumerable keys
99
+ const arr: any[] = [];
100
+ const obj = {};
101
+ expect(shallowEqual(obj, arr)).toBe(true); // Both have 0 enumerable keys
102
+
103
+ const arr2 = [1, 2];
104
+ const obj2 = { 0: 1, 1: 2 };
105
+ expect(shallowEqual(obj2, arr2)).toBe(true); // Same enumerable keys and values
106
+
107
+ // These should return false because of type differences
108
+ expect(
109
+ shallowEqual('hello', { 0: 'h', 1: 'e', 2: 'l', 3: 'l', 4: 'o' }),
110
+ ).toBe(false);
111
+ expect(shallowEqual(42, { valueOf: () => 42 })).toBe(false);
112
+ });
113
+
114
+ it('should return false when comparing objects with primitives', () => {
115
+ expect(shallowEqual({}, null)).toBe(false);
116
+ expect(shallowEqual({}, undefined)).toBe(false);
117
+ expect(shallowEqual({}, 'object')).toBe(false);
118
+ expect(shallowEqual({}, 123)).toBe(false);
119
+ expect(shallowEqual({}, true)).toBe(false);
120
+ });
121
+ });
122
+
123
+ describe('Edge cases', () => {
124
+ it('should handle objects with prototype properties correctly', () => {
125
+ const obj1 = Object.create({ inherited: true });
126
+ obj1.own = 'property';
127
+
128
+ const obj2 = { own: 'property' };
129
+
130
+ expect(shallowEqual(obj1, obj2)).toBe(true); // Only compares own properties
131
+ });
132
+
133
+ it('should handle objects with Symbol properties', () => {
134
+ const sym = Symbol('test');
135
+ const obj1 = { [sym]: 'value', a: 1 };
136
+ const obj2 = { [sym]: 'value', a: 1 };
137
+
138
+ // Symbol properties are not enumerated by Object.keys
139
+ expect(shallowEqual(obj1, obj2)).toBe(true);
140
+ });
141
+
142
+ it('should handle Date objects', () => {
143
+ const date1 = new Date('2023-01-01');
144
+ const date2 = new Date('2023-01-01');
145
+ const sameDate = date1;
146
+
147
+ // Date objects have no enumerable properties, so they're considered equal
148
+ expect(shallowEqual(date1, date2)).toBe(true); // Both have 0 enumerable keys
149
+ expect(shallowEqual(date1, sameDate)).toBe(true); // Same reference
150
+ });
151
+
152
+ it('should handle RegExp objects', () => {
153
+ const regex1 = /test/g;
154
+ const regex2 = /test/g;
155
+ const sameRegex = regex1;
156
+
157
+ // RegExp objects have no enumerable properties, so they're considered equal
158
+ expect(shallowEqual(regex1, regex2)).toBe(true); // Both have 0 enumerable keys
159
+ expect(shallowEqual(regex1, sameRegex)).toBe(true); // Same reference
160
+ });
161
+
162
+ it('should handle functions', () => {
163
+ const fn1 = () => {};
164
+ const fn2 = () => {};
165
+ const sameFn = fn1;
166
+
167
+ expect(shallowEqual(fn1, fn2)).toBe(false);
168
+ expect(shallowEqual(fn1, sameFn)).toBe(true);
169
+ });
170
+ });
171
+ });
@@ -0,0 +1,144 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { generateUUID } from '../uuid';
3
+
4
+ describe('generateUUID', () => {
5
+ const UUID_REGEX =
6
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7
+
8
+ describe('crypto.randomUUID available', () => {
9
+ it('should use crypto.randomUUID when available', () => {
10
+ const mockRandomUUID = vi.fn(
11
+ () => '550e8400-e29b-41d4-a716-446655440000',
12
+ );
13
+
14
+ // Mock the crypto object
15
+ vi.stubGlobal('crypto', {
16
+ randomUUID: mockRandomUUID,
17
+ });
18
+
19
+ const uuid = generateUUID();
20
+
21
+ expect(mockRandomUUID).toHaveBeenCalled();
22
+ expect(uuid).toBe('550e8400-e29b-41d4-a716-446655440000');
23
+
24
+ vi.unstubAllGlobals();
25
+ });
26
+
27
+ it('should return valid UUID v4 format from native crypto.randomUUID', () => {
28
+ // Test with real crypto.randomUUID if available
29
+ if (typeof crypto !== 'undefined' && crypto.randomUUID !== undefined) {
30
+ const uuid = generateUUID();
31
+ expect(uuid).toMatch(UUID_REGEX);
32
+ } else {
33
+ // Skip test if crypto.randomUUID is not available in test environment
34
+ expect(true).toBe(true);
35
+ }
36
+ });
37
+ });
38
+
39
+ describe('Fallback implementation', () => {
40
+ beforeEach(() => {
41
+ // Remove crypto to force fallback
42
+ vi.stubGlobal('crypto', undefined);
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.unstubAllGlobals();
47
+ });
48
+
49
+ it('should generate valid UUID v4 format using fallback', () => {
50
+ const uuid = generateUUID();
51
+ expect(uuid).toMatch(UUID_REGEX);
52
+ });
53
+
54
+ it('should generate unique UUIDs', () => {
55
+ const uuids = new Set();
56
+ const count = 1000;
57
+
58
+ for (let i = 0; i < count; i++) {
59
+ uuids.add(generateUUID());
60
+ }
61
+
62
+ // All generated UUIDs should be unique
63
+ expect(uuids.size).toBe(count);
64
+ });
65
+
66
+ it('should have correct version and variant bits', () => {
67
+ for (let i = 0; i < 100; i++) {
68
+ const uuid = generateUUID();
69
+ const parts = uuid.split('-');
70
+
71
+ // Version 4 UUID should have '4' in the third group
72
+ expect(parts[2][0]).toBe('4');
73
+
74
+ // Variant bits should be correct (8, 9, a, or b)
75
+ const variantChar = parts[3][0].toLowerCase();
76
+ expect(['8', '9', 'a', 'b']).toContain(variantChar);
77
+ }
78
+ });
79
+
80
+ it('should have correct length and format', () => {
81
+ const uuid = generateUUID();
82
+
83
+ expect(uuid.length).toBe(36);
84
+ expect(uuid.split('-').length).toBe(5);
85
+
86
+ const parts = uuid.split('-');
87
+ expect(parts[0].length).toBe(8);
88
+ expect(parts[1].length).toBe(4);
89
+ expect(parts[2].length).toBe(4);
90
+ expect(parts[3].length).toBe(4);
91
+ expect(parts[4].length).toBe(12);
92
+ });
93
+ });
94
+
95
+ describe('Environment compatibility', () => {
96
+ it('should work when crypto is defined but randomUUID is not', () => {
97
+ // Mock crypto without randomUUID
98
+ vi.stubGlobal('crypto', {});
99
+
100
+ const uuid = generateUUID();
101
+ expect(uuid).toMatch(UUID_REGEX);
102
+
103
+ vi.unstubAllGlobals();
104
+ });
105
+
106
+ it('should work with different Math.random implementations', () => {
107
+ // First remove crypto to ensure fallback
108
+ vi.stubGlobal('crypto', undefined);
109
+
110
+ const originalMathRandom = Math.random;
111
+ let callCount = 0;
112
+
113
+ // Mock Math.random to return predictable values
114
+ Math.random = vi.fn(() => {
115
+ callCount++;
116
+ return (callCount % 16) / 16;
117
+ });
118
+
119
+ const uuid = generateUUID();
120
+ expect(uuid).toMatch(UUID_REGEX);
121
+ expect(Math.random).toHaveBeenCalled();
122
+
123
+ Math.random = originalMathRandom;
124
+ vi.unstubAllGlobals();
125
+ });
126
+ });
127
+
128
+ describe('Performance', () => {
129
+ it('should generate UUIDs efficiently', () => {
130
+ const startTime = performance.now();
131
+ const iterations = 10000;
132
+
133
+ for (let i = 0; i < iterations; i++) {
134
+ generateUUID();
135
+ }
136
+
137
+ const endTime = performance.now();
138
+ const totalTime = endTime - startTime;
139
+
140
+ // Should generate 10,000 UUIDs in less than 100ms
141
+ expect(totalTime).toBeLessThan(100);
142
+ });
143
+ });
144
+ });