|
16 | 16 |
|
17 | 17 | import { vi, describe, it, expect, beforeEach } from 'vitest'; |
18 | 18 | import React from 'react'; |
19 | | -import { act } from '@testing-library/react'; |
| 19 | +import { act, waitFor } from '@testing-library/react'; |
20 | 20 | import { renderHook } from '@testing-library/react'; |
21 | | -import { OptimizelyContext, ProviderStateStore } from '../provider/index'; |
| 21 | +import { OptimizelyContext, ProviderStateStore, OptimizelyProvider } from '../provider/index'; |
| 22 | +import { REACT_CLIENT_META } from '../client/index'; |
22 | 23 | import { useDecide } from './useDecide'; |
23 | 24 | import type { |
24 | 25 | OptimizelyUserContext, |
@@ -68,6 +69,49 @@ function createMockClient(hasConfig = false): Client { |
68 | 69 | } as unknown as Client; |
69 | 70 | } |
70 | 71 |
|
| 72 | +/** |
| 73 | + * Creates a mock client with notification center support and wraps it in OptimizelyProvider. |
| 74 | + * Used for integration-style tests that need the full Provider lifecycle. |
| 75 | + */ |
| 76 | +function createProviderWrapper(mockUserContext: OptimizelyUserContext) { |
| 77 | + let configUpdateCallback: (() => void) | undefined; |
| 78 | + |
| 79 | + const client = { |
| 80 | + getOptimizelyConfig: vi.fn().mockReturnValue({ revision: '1' }), |
| 81 | + createUserContext: vi.fn().mockReturnValue(mockUserContext), |
| 82 | + onReady: vi.fn().mockResolvedValue(undefined), |
| 83 | + isOdpIntegrated: vi.fn().mockReturnValue(false), |
| 84 | + notificationCenter: { |
| 85 | + addNotificationListener: vi.fn().mockImplementation((type: string, cb: () => void) => { |
| 86 | + if (type === 'OPTIMIZELY_CONFIG_UPDATE') { |
| 87 | + configUpdateCallback = cb; |
| 88 | + } |
| 89 | + return 1; |
| 90 | + }), |
| 91 | + removeNotificationListener: vi.fn(), |
| 92 | + }, |
| 93 | + } as unknown as Client; |
| 94 | + |
| 95 | + (client as unknown as Record<symbol, unknown>)[REACT_CLIENT_META] = { |
| 96 | + hasOdpManager: false, |
| 97 | + hasVuidManager: false, |
| 98 | + }; |
| 99 | + |
| 100 | + function Wrapper({ children }: { children: React.ReactNode }) { |
| 101 | + return ( |
| 102 | + <OptimizelyProvider client={client} user={{ id: 'user-1' }}> |
| 103 | + {children} |
| 104 | + </OptimizelyProvider> |
| 105 | + ); |
| 106 | + } |
| 107 | + |
| 108 | + return { |
| 109 | + wrapper: Wrapper, |
| 110 | + client, |
| 111 | + fireConfigUpdate: () => configUpdateCallback?.(), |
| 112 | + }; |
| 113 | +} |
| 114 | + |
71 | 115 | function createWrapper(store: ProviderStateStore, client: Client) { |
72 | 116 | const contextValue: OptimizelyContextValue = { store, client }; |
73 | 117 |
|
@@ -346,6 +390,38 @@ describe('useDecide', () => { |
346 | 390 | expect(result.current.decision).toBe(MOCK_DECISION); |
347 | 391 | }); |
348 | 392 |
|
| 393 | + it('should re-evaluate decision when OPTIMIZELY_CONFIG_UPDATE fires from the client', async () => { |
| 394 | + const mockUserContext = createMockUserContext(); |
| 395 | + const { wrapper, fireConfigUpdate } = createProviderWrapper(mockUserContext); |
| 396 | + |
| 397 | + const { result } = renderHook(() => useDecide('flag_1'), { wrapper }); |
| 398 | + |
| 399 | + // Wait for Provider's onReady + UserContextManager + queueMicrotask chain to complete |
| 400 | + await waitFor(() => { |
| 401 | + expect(result.current.isLoading).toBe(false); |
| 402 | + }); |
| 403 | + |
| 404 | + expect(result.current.decision).toBe(MOCK_DECISION); |
| 405 | + const callCountBeforeUpdate = (mockUserContext.decide as ReturnType<typeof vi.fn>).mock.calls.length; |
| 406 | + |
| 407 | + // Simulate a new datafile with a different decision |
| 408 | + const updatedDecision: OptimizelyDecision = { |
| 409 | + ...MOCK_DECISION, |
| 410 | + variationKey: 'variation_2', |
| 411 | + variables: { color: 'blue' }, |
| 412 | + }; |
| 413 | + (mockUserContext.decide as ReturnType<typeof vi.fn>).mockReturnValue(updatedDecision); |
| 414 | + |
| 415 | + // Fire the config update notification (as the SDK would on datafile poll) |
| 416 | + await act(async () => { |
| 417 | + fireConfigUpdate(); |
| 418 | + }); |
| 419 | + |
| 420 | + expect(mockUserContext.decide).toHaveBeenCalledTimes(callCountBeforeUpdate + 1); |
| 421 | + expect(result.current.decision).toBe(updatedDecision); |
| 422 | + expect(result.current.isLoading).toBe(false); |
| 423 | + }); |
| 424 | + |
349 | 425 | describe('forced decision reactivity', () => { |
350 | 426 | it('should re-evaluate when setForcedDecision is called for the same flagKey', () => { |
351 | 427 | mockClient = createMockClient(true); |
|
0 commit comments