@openmrs/esm-state 8.0.1-pre.3518 → 8.0.1-pre.3529

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.
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 3 files with swc (77.01ms)
1
+ [0] Successfully compiled: 4 files with swc (73.5ms)
2
2
  [0] swc --strip-leading-paths src -d dist exited with code 0
3
3
  [1] tsc --project tsconfig.build.json exited with code 0
package/dist/state.js CHANGED
@@ -1,17 +1,10 @@
1
1
  /** @module @category Store */ import { shallowEqual } from "@openmrs/esm-utils";
2
2
  import { createStore } from "zustand/vanilla";
3
+ import { isTestEnvironment } from "./utils.js";
3
4
  const availableStores = {};
4
- // Check if we're in a test environment (Vitest or Jest)
5
- const isTestEnvironment = ()=>{
6
- try {
7
- return process.env.NODE_ENV === 'test' || typeof process !== 'undefined' && (process.env.VITEST === 'true' || process.env.JEST_WORKER_ID !== undefined) || typeof globalThis !== 'undefined' && ('__vitest_worker__' in globalThis || '__jest__' in globalThis);
8
- } catch {
9
- return false;
10
- }
11
- };
12
5
  // spaEnv isn't available immediately. Wait a bit before making stores available
13
6
  // on window in development mode.
14
- setTimeout(()=>{
7
+ globalThis.setTimeout?.(()=>{
15
8
  if (typeof window !== 'undefined' && window.spaEnv === 'development') {
16
9
  window['stores'] = availableStores;
17
10
  }
@@ -94,10 +87,12 @@ export function subscribeTo(...args) {
94
87
  const [store, select, handle] = args;
95
88
  const handler = typeof handle === 'undefined' ? select : handle;
96
89
  const selector = typeof handle === 'undefined' ? (state)=>state : select;
97
- handler(selector(store.getState()));
98
- return store.subscribe((state, previous)=>{
90
+ let previous = selector(store.getState());
91
+ handler(previous);
92
+ return store.subscribe((state)=>{
99
93
  const current = selector(state);
100
94
  if (!shallowEqual(previous, current)) {
95
+ previous = current;
101
96
  handler(current);
102
97
  }
103
98
  });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Check if we're in a test environment (Vitest or Jest)
3
+ * Exported in a separate file so it can be mocked in tests
4
+ * @internal
5
+ */
6
+ export declare function isTestEnvironment(): boolean;
package/dist/utils.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Check if we're in a test environment (Vitest or Jest)
3
+ * Exported in a separate file so it can be mocked in tests
4
+ * @internal
5
+ */ export function isTestEnvironment() {
6
+ try {
7
+ return process.env.NODE_ENV === 'test' || typeof process !== 'undefined' && (process.env.VITEST === 'true' || process.env.JEST_WORKER_ID !== undefined) || typeof globalThis !== 'undefined' && ('__vitest_worker__' in globalThis || '__jest__' in globalThis);
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-state",
3
- "version": "8.0.1-pre.3518",
3
+ "version": "8.0.1-pre.3529",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Frontend stores & state management for OpenMRS",
6
6
  "type": "module",
@@ -23,6 +23,9 @@
23
23
  },
24
24
  "source": true,
25
25
  "scripts": {
26
+ "test": "cross-env TZ=UTC vitest run --passWithNoTests",
27
+ "test:watch": "cross-env TZ=UTC vitest watch --passWithNoTests",
28
+ "coverage": "cross-env TZ=UTC vitest run --coverage --passWithNoTests",
26
29
  "build": "rimraf dist && concurrently \"swc --strip-leading-paths src -d dist\" \"tsc --project tsconfig.build.json\"",
27
30
  "build:development": "rimraf dist && concurrently \"swc --strip-leading-paths src -d dist\" \"tsc --project tsconfig.build.json\"",
28
31
  "typescript": "tsc --project tsconfig.build.json",
@@ -58,13 +61,15 @@
58
61
  "@openmrs/esm-utils": "6.x"
59
62
  },
60
63
  "devDependencies": {
61
- "@openmrs/esm-globals": "8.0.1-pre.3518",
62
- "@openmrs/esm-utils": "8.0.1-pre.3518",
64
+ "@openmrs/esm-globals": "8.0.1-pre.3529",
65
+ "@openmrs/esm-utils": "8.0.1-pre.3529",
63
66
  "@swc/cli": "^0.7.7",
64
67
  "@swc/core": "^1.11.29",
68
+ "@vitest/coverage-v8": "^4.0.7",
65
69
  "concurrently": "^9.1.2",
66
70
  "cross-env": "^7.0.3",
67
- "rimraf": "^6.0.1"
71
+ "rimraf": "^6.0.1",
72
+ "vitest": "^4.0.7"
68
73
  },
69
74
  "stableVersion": "8.0.0"
70
75
  }
@@ -0,0 +1,531 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { createStore } from 'zustand/vanilla';
3
+ import { createGlobalStore, registerGlobalStore, getGlobalStore, subscribeTo } from './state';
4
+
5
+ // Helper to generate unique store names for test isolation
6
+ let storeCounter = 0;
7
+ function getUniqueStoreName(prefix: string = 'test-store'): string {
8
+ return `${prefix}-${++storeCounter}`;
9
+ }
10
+
11
+ describe('createGlobalStore', () => {
12
+ it('should create a new global store with initial state', () => {
13
+ const storeName = getUniqueStoreName();
14
+ const initialState = { count: 0, name: 'test' };
15
+
16
+ const store = createGlobalStore(storeName, initialState);
17
+
18
+ expect(store).toBeDefined();
19
+ expect(store.getState()).toEqual(initialState);
20
+ });
21
+
22
+ it('should allow updating store state', () => {
23
+ const storeName = getUniqueStoreName();
24
+ const initialState = { count: 0 };
25
+
26
+ const store = createGlobalStore(storeName, initialState);
27
+ store.setState({ count: 5 });
28
+
29
+ expect(store.getState().count).toBe(5);
30
+ });
31
+
32
+ it('should warn when creating duplicate store name in non-test environment', async () => {
33
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
34
+
35
+ // Reset modules to clear cache
36
+ vi.resetModules();
37
+
38
+ // Mock the utils module to return false for isTestEnvironment
39
+ vi.doMock('./utils', () => ({
40
+ isTestEnvironment: () => false,
41
+ }));
42
+
43
+ // Dynamic import to get the mocked version
44
+ const { createGlobalStore: createGlobalStoreNonTest } = await import('./state');
45
+ const storeName = getUniqueStoreName();
46
+
47
+ createGlobalStoreNonTest(storeName, { value: 1 });
48
+ consoleSpy.mockClear();
49
+
50
+ createGlobalStoreNonTest(storeName, { value: 2 });
51
+
52
+ expect(consoleSpy).toHaveBeenCalledWith(
53
+ expect.stringContaining(`Attempted to override the existing store ${storeName}`),
54
+ );
55
+
56
+ consoleSpy.mockRestore();
57
+ vi.doUnmock('./utils');
58
+ vi.resetModules();
59
+ });
60
+
61
+ it('should allow duplicate stores in test environment without warning', () => {
62
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
63
+ const storeName = getUniqueStoreName();
64
+
65
+ // Ensure we're in test environment (should already be true)
66
+ process.env.NODE_ENV = 'test';
67
+
68
+ createGlobalStore(storeName, { value: 1 });
69
+ createGlobalStore(storeName, { value: 2 });
70
+
71
+ expect(consoleSpy).not.toHaveBeenCalled();
72
+
73
+ consoleSpy.mockRestore();
74
+ });
75
+
76
+ it('should mark store as active', () => {
77
+ const storeName = getUniqueStoreName();
78
+ const store = createGlobalStore(storeName, { active: true });
79
+
80
+ // Store should be retrievable and active
81
+ const retrieved = getGlobalStore(storeName);
82
+ expect(retrieved).toBe(store);
83
+ });
84
+
85
+ it('should reactivate inactive store with new initial state', () => {
86
+ const storeName = getUniqueStoreName();
87
+
88
+ // Create initial store
89
+ const store1 = createGlobalStore(storeName, { value: 1 });
90
+
91
+ // Get it (which should mark it as not active when using getGlobalStore on non-existent)
92
+ // Actually, we need to manually deactivate it by creating through getGlobalStore first
93
+ // Let's create a scenario where the store exists but is inactive
94
+
95
+ // Use getGlobalStore to create an inactive store
96
+ const inactiveStoreName = getUniqueStoreName();
97
+ const inactiveStore = getGlobalStore(inactiveStoreName, { value: 0 });
98
+
99
+ // Now createGlobalStore should reactivate it
100
+ const reactivated = createGlobalStore(inactiveStoreName, { value: 5 });
101
+
102
+ expect(reactivated).toBe(inactiveStore);
103
+ expect(reactivated.getState().value).toBe(5);
104
+ });
105
+
106
+ it('should use provided initial state', () => {
107
+ const storeName = getUniqueStoreName();
108
+ const complexState = {
109
+ user: { name: 'John', age: 30 },
110
+ settings: { theme: 'dark' },
111
+ items: [1, 2, 3],
112
+ };
113
+
114
+ const store = createGlobalStore(storeName, complexState);
115
+
116
+ expect(store.getState()).toEqual(complexState);
117
+ });
118
+
119
+ it('should return the same store instance when recreating with same name', () => {
120
+ const storeName = getUniqueStoreName();
121
+
122
+ const store1 = createGlobalStore(storeName, { value: 1 });
123
+ const store2 = createGlobalStore(storeName, { value: 2 });
124
+
125
+ // Should be the same instance
126
+ expect(store1).toBe(store2);
127
+ });
128
+ });
129
+
130
+ describe('registerGlobalStore', () => {
131
+ it('should register an existing Zustand store', () => {
132
+ const storeName = getUniqueStoreName();
133
+ const zustandStore = createStore(() => ({ count: 42 }));
134
+
135
+ const registered = registerGlobalStore(storeName, zustandStore);
136
+
137
+ expect(registered).toBe(zustandStore);
138
+ expect(registered.getState().count).toBe(42);
139
+ });
140
+
141
+ it('should make registered store retrievable via getGlobalStore', () => {
142
+ const storeName = getUniqueStoreName();
143
+ const zustandStore = createStore(() => ({ value: 'test' }));
144
+
145
+ registerGlobalStore(storeName, zustandStore);
146
+ const retrieved = getGlobalStore(storeName);
147
+
148
+ expect(retrieved).toBe(zustandStore);
149
+ });
150
+
151
+ it('should warn when registering duplicate store name in non-test environment', async () => {
152
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
153
+
154
+ // Reset modules to clear cache
155
+ vi.resetModules();
156
+
157
+ // Mock the utils module to return false for isTestEnvironment
158
+ vi.doMock('./utils', () => ({
159
+ isTestEnvironment: () => false,
160
+ }));
161
+
162
+ // Dynamic import to get the mocked version
163
+ const { registerGlobalStore: registerGlobalStoreNonTest } = await import('./state');
164
+ const storeName = getUniqueStoreName();
165
+
166
+ const store1 = createStore(() => ({ value: 1 }));
167
+ const store2 = createStore(() => ({ value: 2 }));
168
+
169
+ registerGlobalStoreNonTest(storeName, store1);
170
+ consoleSpy.mockClear();
171
+
172
+ registerGlobalStoreNonTest(storeName, store2);
173
+
174
+ expect(consoleSpy).toHaveBeenCalledWith(
175
+ expect.stringContaining(`Attempted to override the existing store ${storeName}`),
176
+ );
177
+
178
+ consoleSpy.mockRestore();
179
+ vi.doUnmock('./utils');
180
+ vi.resetModules();
181
+ });
182
+
183
+ it('should allow duplicate registrations in test environment', () => {
184
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
185
+ const storeName = getUniqueStoreName();
186
+
187
+ process.env.NODE_ENV = 'test';
188
+
189
+ const store1 = createStore(() => ({ value: 1 }));
190
+ const store2 = createStore(() => ({ value: 2 }));
191
+
192
+ registerGlobalStore(storeName, store1);
193
+ registerGlobalStore(storeName, store2);
194
+
195
+ expect(consoleSpy).not.toHaveBeenCalled();
196
+
197
+ consoleSpy.mockRestore();
198
+ });
199
+
200
+ it('should mark registered store as active', () => {
201
+ const storeName = getUniqueStoreName();
202
+ const zustandStore = createStore(() => ({ active: true }));
203
+
204
+ registerGlobalStore(storeName, zustandStore);
205
+
206
+ // Should be retrievable
207
+ const retrieved = getGlobalStore(storeName);
208
+ expect(retrieved).toBe(zustandStore);
209
+ });
210
+
211
+ it('should replace inactive store with new registration', () => {
212
+ const storeName = getUniqueStoreName();
213
+
214
+ // Create inactive store via getGlobalStore
215
+ const inactiveStore = getGlobalStore(storeName, { value: 0 });
216
+
217
+ // Register a new store with same name
218
+ const newStore = createStore(() => ({ value: 99 }));
219
+ const registered = registerGlobalStore(storeName, newStore);
220
+
221
+ expect(registered).toBe(newStore);
222
+ expect(registered.getState().value).toBe(99);
223
+ });
224
+
225
+ it('should return the same store instance when re-registering active store', () => {
226
+ const storeName = getUniqueStoreName();
227
+ const store1 = createStore(() => ({ value: 1 }));
228
+ const store2 = createStore(() => ({ value: 2 }));
229
+
230
+ const registered1 = registerGlobalStore(storeName, store1);
231
+ const registered2 = registerGlobalStore(storeName, store2);
232
+
233
+ // Should return the first registered store (active)
234
+ expect(registered1).toBe(registered2);
235
+ expect(registered1).toBe(store1);
236
+ });
237
+ });
238
+
239
+ describe('getGlobalStore', () => {
240
+ it('should return existing store', () => {
241
+ const storeName = getUniqueStoreName();
242
+ const created = createGlobalStore<{ value: string }>(storeName, { value: 'exists' });
243
+
244
+ const retrieved = getGlobalStore<{ value: string }>(storeName);
245
+
246
+ expect(retrieved).toBe(created);
247
+ expect(retrieved.getState().value).toBe('exists');
248
+ });
249
+
250
+ it('should create store on demand if not exists', () => {
251
+ const storeName = getUniqueStoreName();
252
+
253
+ const store = getGlobalStore(storeName, { created: 'on-demand' });
254
+
255
+ expect(store).toBeDefined();
256
+ expect(store.getState().created).toBe('on-demand');
257
+ });
258
+
259
+ it('should use fallback state when creating on demand', () => {
260
+ const storeName = getUniqueStoreName();
261
+ const fallbackState = { name: 'fallback', count: 123 };
262
+
263
+ const store = getGlobalStore(storeName, fallbackState);
264
+
265
+ expect(store.getState()).toEqual(fallbackState);
266
+ });
267
+
268
+ it('should create store with empty object when no fallback provided', () => {
269
+ const storeName = getUniqueStoreName();
270
+
271
+ const store = getGlobalStore(storeName);
272
+
273
+ expect(store).toBeDefined();
274
+ expect(store.getState()).toEqual({});
275
+ });
276
+
277
+ it('should mark on-demand created store as inactive', () => {
278
+ const storeName = getUniqueStoreName();
279
+
280
+ // Create via getGlobalStore (inactive)
281
+ const store1 = getGlobalStore(storeName, { value: 1 });
282
+
283
+ // Now createGlobalStore should be able to reactivate and replace state
284
+ const store2 = createGlobalStore(storeName, { value: 2 });
285
+
286
+ expect(store1).toBe(store2);
287
+ expect(store2.getState().value).toBe(2);
288
+ });
289
+
290
+ it('should return existing active store even when fallback is provided', () => {
291
+ const storeName = getUniqueStoreName();
292
+
293
+ const created = createGlobalStore(storeName, { value: 'original' });
294
+ const retrieved = getGlobalStore(storeName, { value: 'fallback' });
295
+
296
+ expect(retrieved).toBe(created);
297
+ expect(retrieved.getState().value).toBe('original');
298
+ });
299
+ });
300
+
301
+ describe('subscribeTo', () => {
302
+ describe('full state subscription', () => {
303
+ it('should subscribe to entire store state', () => {
304
+ const storeName = getUniqueStoreName();
305
+ const store = createGlobalStore(storeName, { count: 0 });
306
+ const handler = vi.fn();
307
+
308
+ subscribeTo(store, handler);
309
+
310
+ // Handler should be called immediately with current state
311
+ expect(handler).toHaveBeenCalledWith({ count: 0 });
312
+ });
313
+
314
+ it('should call handler on state change', () => {
315
+ const storeName = getUniqueStoreName();
316
+ const store = createGlobalStore(storeName, { count: 0 });
317
+ const handler = vi.fn();
318
+
319
+ subscribeTo(store, handler);
320
+ handler.mockClear(); // Clear initial call
321
+
322
+ store.setState({ count: 1 });
323
+
324
+ expect(handler).toHaveBeenCalledWith({ count: 1 });
325
+ });
326
+
327
+ it('should return unsubscribe function', () => {
328
+ const storeName = getUniqueStoreName();
329
+ const store = createGlobalStore(storeName, { count: 0 });
330
+ const handler = vi.fn();
331
+
332
+ const unsubscribe = subscribeTo(store, handler);
333
+
334
+ expect(typeof unsubscribe).toBe('function');
335
+ });
336
+
337
+ it('should stop calling handler after unsubscribe', () => {
338
+ const storeName = getUniqueStoreName();
339
+ const store = createGlobalStore(storeName, { count: 0 });
340
+ const handler = vi.fn();
341
+
342
+ const unsubscribe = subscribeTo(store, handler);
343
+ handler.mockClear();
344
+
345
+ unsubscribe();
346
+
347
+ store.setState({ count: 1 });
348
+ store.setState({ count: 2 });
349
+
350
+ expect(handler).not.toHaveBeenCalled();
351
+ });
352
+
353
+ it('should handle multiple subscribers', () => {
354
+ const storeName = getUniqueStoreName();
355
+ const store = createGlobalStore(storeName, { value: 'a' });
356
+ const handler1 = vi.fn();
357
+ const handler2 = vi.fn();
358
+
359
+ subscribeTo(store, handler1);
360
+ subscribeTo(store, handler2);
361
+
362
+ handler1.mockClear();
363
+ handler2.mockClear();
364
+
365
+ store.setState({ value: 'b' });
366
+
367
+ expect(handler1).toHaveBeenCalledWith({ value: 'b' });
368
+ expect(handler2).toHaveBeenCalledWith({ value: 'b' });
369
+ });
370
+ });
371
+
372
+ describe('selected state subscription', () => {
373
+ it('should subscribe to selected portion of state', () => {
374
+ const storeName = getUniqueStoreName();
375
+ const store = createGlobalStore(storeName, { user: { name: 'John' }, count: 5 });
376
+ const handler = vi.fn();
377
+ const selector = (state: any) => state.user;
378
+
379
+ subscribeTo(store, selector, handler);
380
+
381
+ expect(handler).toHaveBeenCalledWith({ name: 'John' });
382
+ });
383
+
384
+ it('should only call handler when selected state changes', () => {
385
+ const storeName = getUniqueStoreName();
386
+ const store = createGlobalStore(storeName, { user: { name: 'John' }, count: 0 });
387
+ const handler = vi.fn();
388
+ const selector = (state: any) => state.user;
389
+
390
+ subscribeTo(store, selector, handler);
391
+ handler.mockClear();
392
+
393
+ // Change unrelated state
394
+ store.setState({ user: { name: 'John' }, count: 1 });
395
+
396
+ // Handler should not be called (user didn't change)
397
+ expect(handler).not.toHaveBeenCalled();
398
+ });
399
+
400
+ it('should call handler when selected state changes', () => {
401
+ const storeName = getUniqueStoreName();
402
+ const store = createGlobalStore(storeName, { user: { name: 'John' }, count: 0 });
403
+ const handler = vi.fn();
404
+ const selector = (state: any) => state.user;
405
+
406
+ subscribeTo(store, selector, handler);
407
+ handler.mockClear();
408
+
409
+ store.setState({ user: { name: 'Jane' }, count: 0 });
410
+
411
+ expect(handler).toHaveBeenCalledWith({ name: 'Jane' });
412
+ });
413
+
414
+ it('should use shallow equality for change detection', () => {
415
+ const storeName = getUniqueStoreName();
416
+ const store = createGlobalStore(storeName, { items: [1, 2, 3], meta: 'data' });
417
+ const handler = vi.fn();
418
+ const selector = (state: any) => state.items;
419
+
420
+ subscribeTo(store, selector, handler);
421
+ handler.mockClear();
422
+
423
+ // Set same array reference
424
+ const sameItems = store.getState().items;
425
+ store.setState({ items: sameItems, meta: 'changed' });
426
+
427
+ // Handler should not be called (same reference)
428
+ expect(handler).not.toHaveBeenCalled();
429
+
430
+ // Set new array with same content - shallowEqual compares array elements
431
+ store.setState({ items: [1, 2, 3], meta: 'changed' });
432
+
433
+ // Handler should NOT be called (arrays are shallowEqual)
434
+ expect(handler).not.toHaveBeenCalled();
435
+
436
+ // Set new array with different content
437
+ store.setState({ items: [1, 2, 3, 4], meta: 'changed' });
438
+
439
+ // Handler should be called (different array content)
440
+ expect(handler).toHaveBeenCalled();
441
+ });
442
+
443
+ it('should return unsubscribe function for selected subscription', () => {
444
+ const storeName = getUniqueStoreName();
445
+ const store = createGlobalStore(storeName, { a: 1, b: 2 });
446
+ const handler = vi.fn();
447
+ const selector = (state: any) => state.a;
448
+
449
+ const unsubscribe = subscribeTo(store, selector, handler);
450
+
451
+ expect(typeof unsubscribe).toBe('function');
452
+
453
+ handler.mockClear();
454
+ unsubscribe();
455
+
456
+ store.setState({ a: 5, b: 2 });
457
+ expect(handler).not.toHaveBeenCalled();
458
+ });
459
+
460
+ it('should work with primitive value selectors', () => {
461
+ const storeName = getUniqueStoreName();
462
+ const store = createGlobalStore(storeName, { count: 0, name: 'test' });
463
+ const handler = vi.fn();
464
+ const selector = (state: any) => state.count;
465
+
466
+ subscribeTo(store, selector, handler);
467
+
468
+ expect(handler).toHaveBeenCalledWith(0);
469
+
470
+ handler.mockClear();
471
+ store.setState({ count: 5, name: 'test' });
472
+
473
+ expect(handler).toHaveBeenCalledWith(5);
474
+ });
475
+
476
+ it('should handle computed/derived values from selector', () => {
477
+ const storeName = getUniqueStoreName();
478
+ const store = createGlobalStore(storeName, { firstName: 'John', lastName: 'Doe' });
479
+ const handler = vi.fn();
480
+ const selector = (state: any) => `${state.firstName} ${state.lastName}`;
481
+
482
+ subscribeTo(store, selector, handler);
483
+
484
+ expect(handler).toHaveBeenCalledWith('John Doe');
485
+
486
+ handler.mockClear();
487
+ store.setState({ firstName: 'Jane', lastName: 'Doe' });
488
+
489
+ expect(handler).toHaveBeenCalledWith('Jane Doe');
490
+ });
491
+ });
492
+
493
+ describe('edge cases', () => {
494
+ it('should handle rapid state updates', () => {
495
+ const storeName = getUniqueStoreName();
496
+ const store = createGlobalStore(storeName, { count: 0 });
497
+ const handler = vi.fn();
498
+
499
+ subscribeTo(store, handler);
500
+ handler.mockClear();
501
+
502
+ // Rapid updates
503
+ for (let i = 1; i <= 10; i++) {
504
+ store.setState({ count: i });
505
+ }
506
+
507
+ expect(handler).toHaveBeenCalledTimes(10);
508
+ expect(handler).toHaveBeenLastCalledWith({ count: 10 });
509
+ });
510
+
511
+ it('should handle undefined state values', () => {
512
+ const storeName = getUniqueStoreName();
513
+ const store = createGlobalStore(storeName, { value: undefined as string | undefined });
514
+ const handler = vi.fn();
515
+
516
+ subscribeTo(store, handler);
517
+
518
+ expect(handler).toHaveBeenCalledWith({ value: undefined });
519
+ });
520
+
521
+ it('should handle null state values', () => {
522
+ const storeName = getUniqueStoreName();
523
+ const store = createGlobalStore(storeName, { value: null as string | null });
524
+ const handler = vi.fn();
525
+
526
+ subscribeTo(store, handler);
527
+
528
+ expect(handler).toHaveBeenCalledWith({ value: null });
529
+ });
530
+ });
531
+ });
package/src/state.ts CHANGED
@@ -3,6 +3,7 @@ import type {} from '@openmrs/esm-globals';
3
3
  import { shallowEqual } from '@openmrs/esm-utils';
4
4
  import type { StoreApi } from 'zustand/vanilla';
5
5
  import { createStore } from 'zustand/vanilla';
6
+ import { isTestEnvironment } from './utils';
6
7
 
7
8
  interface StoreEntity {
8
9
  value: StoreApi<unknown>;
@@ -11,22 +12,9 @@ interface StoreEntity {
11
12
 
12
13
  const availableStores: Record<string, StoreEntity> = {};
13
14
 
14
- // Check if we're in a test environment (Vitest or Jest)
15
- const isTestEnvironment = () => {
16
- try {
17
- return (
18
- process.env.NODE_ENV === 'test' ||
19
- (typeof process !== 'undefined' && (process.env.VITEST === 'true' || process.env.JEST_WORKER_ID !== undefined)) ||
20
- (typeof globalThis !== 'undefined' && ('__vitest_worker__' in globalThis || '__jest__' in globalThis))
21
- );
22
- } catch {
23
- return false;
24
- }
25
- };
26
-
27
15
  // spaEnv isn't available immediately. Wait a bit before making stores available
28
16
  // on window in development mode.
29
- setTimeout(() => {
17
+ globalThis.setTimeout?.(() => {
30
18
  if (typeof window !== 'undefined' && window.spaEnv === 'development') {
31
19
  window['stores'] = availableStores;
32
20
  }
@@ -134,11 +122,13 @@ export function subscribeTo<T, U>(...args: SubscribeToArgs<T, U>): () => void {
134
122
  const handler = typeof handle === 'undefined' ? (select as unknown as (state: U) => void) : handle;
135
123
  const selector = typeof handle === 'undefined' ? (state: T) => state as unknown as U : (select as (state: T) => U);
136
124
 
137
- handler(selector(store.getState()));
138
- return store.subscribe((state, previous) => {
125
+ let previous = selector(store.getState());
126
+ handler(previous);
127
+ return store.subscribe((state) => {
139
128
  const current = selector(state);
140
129
 
141
130
  if (!shallowEqual(previous, current)) {
131
+ previous = current;
142
132
  handler(current);
143
133
  }
144
134
  });
package/src/utils.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Check if we're in a test environment (Vitest or Jest)
3
+ * Exported in a separate file so it can be mocked in tests
4
+ * @internal
5
+ */
6
+ export function isTestEnvironment() {
7
+ try {
8
+ return (
9
+ process.env.NODE_ENV === 'test' ||
10
+ (typeof process !== 'undefined' && (process.env.VITEST === 'true' || process.env.JEST_WORKER_ID !== undefined)) ||
11
+ (typeof globalThis !== 'undefined' && ('__vitest_worker__' in globalThis || '__jest__' in globalThis))
12
+ );
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ mockReset: true,
6
+ },
7
+ });