Skip to content

Commit 23fc3b6

Browse files
[FSSDK-12294] useDecideForKeys, useDecideAll
1 parent e98d020 commit 23fc3b6

11 files changed

Lines changed: 1116 additions & 103 deletions

src/hooks/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ export { useOptimizelyUserContext } from './useOptimizelyUserContext';
1919
export type { UseOptimizelyUserContextResult } from './useOptimizelyUserContext';
2020
export { useDecide } from './useDecide';
2121
export type { UseDecideConfig, UseDecideResult } from './useDecide';
22+
export { useDecideForKeys } from './useDecideForKeys';
23+
export type { UseDecideMultiResult } from './useDecideForKeys';
24+
export { useDecideAll } from './useDecideAll';

src/hooks/testUtils.tsx

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* Copyright 2026, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { vi } from 'vitest';
18+
import React from 'react';
19+
import { OptimizelyContext, ProviderStateStore, OptimizelyProvider } from '../provider/index';
20+
import { REACT_CLIENT_META } from '../client/index';
21+
import type { OptimizelyUserContext, OptimizelyDecision, Client } from '@optimizely/optimizely-sdk';
22+
import type { OptimizelyContextValue } from '../provider/index';
23+
24+
export const MOCK_DECISION: OptimizelyDecision = {
25+
variationKey: 'variation_1',
26+
enabled: true,
27+
variables: { color: 'red' },
28+
ruleKey: 'rule_1',
29+
flagKey: 'flag_1',
30+
userContext: {} as OptimizelyUserContext,
31+
reasons: [],
32+
};
33+
34+
export const MOCK_DECISIONS: Record<string, OptimizelyDecision> = {
35+
flag_1: MOCK_DECISION,
36+
flag_2: {
37+
variationKey: 'variation_2',
38+
enabled: false,
39+
variables: { size: 'large' },
40+
ruleKey: 'rule_2',
41+
flagKey: 'flag_2',
42+
userContext: {} as OptimizelyUserContext,
43+
reasons: [],
44+
},
45+
};
46+
47+
/**
48+
* Creates a mock OptimizelyUserContext with all methods stubbed.
49+
* Override specific methods via the overrides parameter.
50+
*/
51+
export function createMockUserContext(
52+
overrides?: Partial<Record<string, unknown>>,
53+
): OptimizelyUserContext {
54+
return {
55+
getUserId: vi.fn().mockReturnValue('test-user'),
56+
getAttributes: vi.fn().mockReturnValue({}),
57+
fetchQualifiedSegments: vi.fn().mockResolvedValue(true),
58+
decide: vi.fn().mockReturnValue(MOCK_DECISION),
59+
decideAll: vi.fn().mockReturnValue(MOCK_DECISIONS),
60+
decideForKeys: vi.fn().mockImplementation((keys: string[]) => {
61+
const result: Record<string, OptimizelyDecision> = {};
62+
for (const key of keys) {
63+
if (MOCK_DECISIONS[key]) {
64+
result[key] = MOCK_DECISIONS[key];
65+
}
66+
}
67+
return result;
68+
}),
69+
setForcedDecision: vi.fn().mockReturnValue(true),
70+
getForcedDecision: vi.fn(),
71+
removeForcedDecision: vi.fn().mockReturnValue(true),
72+
removeAllForcedDecisions: vi.fn().mockReturnValue(true),
73+
trackEvent: vi.fn(),
74+
getOptimizely: vi.fn(),
75+
setQualifiedSegments: vi.fn(),
76+
getQualifiedSegments: vi.fn().mockReturnValue([]),
77+
qualifiedSegments: null,
78+
...overrides,
79+
} as unknown as OptimizelyUserContext;
80+
}
81+
82+
/**
83+
* Creates a mock Optimizely Client.
84+
* @param hasConfig - If true, getOptimizelyConfig returns a config object; otherwise null.
85+
*/
86+
export function createMockClient(hasConfig = false): Client {
87+
return {
88+
getOptimizelyConfig: vi.fn().mockReturnValue(hasConfig ? { revision: '1' } : null),
89+
createUserContext: vi.fn(),
90+
onReady: vi.fn().mockResolvedValue({ success: true }),
91+
notificationCenter: {},
92+
} as unknown as Client;
93+
}
94+
95+
/**
96+
* Creates a mock client with notification center support and wraps it in OptimizelyProvider.
97+
* Used for integration-style tests that need the full Provider lifecycle.
98+
*/
99+
export function createProviderWrapper(mockUserContext: OptimizelyUserContext) {
100+
let configUpdateCallback: (() => void) | undefined;
101+
102+
const client = {
103+
getOptimizelyConfig: vi.fn().mockReturnValue({ revision: '1' }),
104+
createUserContext: vi.fn().mockReturnValue(mockUserContext),
105+
onReady: vi.fn().mockResolvedValue(undefined),
106+
isOdpIntegrated: vi.fn().mockReturnValue(false),
107+
notificationCenter: {
108+
addNotificationListener: vi.fn().mockImplementation((type: string, cb: () => void) => {
109+
if (type === 'OPTIMIZELY_CONFIG_UPDATE') {
110+
configUpdateCallback = cb;
111+
}
112+
return 1;
113+
}),
114+
removeNotificationListener: vi.fn(),
115+
},
116+
} as unknown as Client;
117+
118+
(client as unknown as Record<symbol, unknown>)[REACT_CLIENT_META] = {
119+
hasOdpManager: false,
120+
hasVuidManager: false,
121+
};
122+
123+
function Wrapper({ children }: { children: React.ReactNode }) {
124+
return (
125+
<OptimizelyProvider client={client} user={{ id: 'user-1' }}>
126+
{children}
127+
</OptimizelyProvider>
128+
);
129+
}
130+
131+
return {
132+
wrapper: Wrapper,
133+
client,
134+
fireConfigUpdate: () => configUpdateCallback?.(),
135+
};
136+
}
137+
138+
/**
139+
* Creates a lightweight wrapper that provides OptimizelyContext directly
140+
* (bypassing Provider lifecycle). Used for unit tests.
141+
*/
142+
export function createWrapper(store: ProviderStateStore, client: Client) {
143+
const contextValue: OptimizelyContextValue = { store, client };
144+
145+
return function Wrapper({ children }: { children: React.ReactNode }) {
146+
return <OptimizelyContext.Provider value={contextValue}>{children}</OptimizelyContext.Provider>;
147+
};
148+
}

src/hooks/useDecide.spec.tsx

Lines changed: 9 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -15,110 +15,18 @@
1515
*/
1616

1717
import { vi, describe, it, expect, beforeEach } from 'vitest';
18-
import React from 'react';
1918
import { act, waitFor } from '@testing-library/react';
2019
import { renderHook } from '@testing-library/react';
21-
import { OptimizelyContext, ProviderStateStore, OptimizelyProvider } from '../provider/index';
22-
import { REACT_CLIENT_META } from '../client/index';
20+
import { ProviderStateStore } from '../provider/index';
2321
import { useDecide } from './useDecide';
24-
import type {
25-
OptimizelyUserContext,
26-
OptimizelyDecision,
27-
Client,
28-
OptimizelyDecideOption,
29-
} from '@optimizely/optimizely-sdk';
30-
import type { OptimizelyContextValue } from '../provider/index';
31-
32-
const MOCK_DECISION: OptimizelyDecision = {
33-
variationKey: 'variation_1',
34-
enabled: true,
35-
variables: { color: 'red' },
36-
ruleKey: 'rule_1',
37-
flagKey: 'flag_1',
38-
userContext: {} as OptimizelyUserContext,
39-
reasons: [],
40-
};
41-
42-
function createMockUserContext(overrides?: Partial<Record<'decide', unknown>>): OptimizelyUserContext {
43-
return {
44-
getUserId: vi.fn().mockReturnValue('test-user'),
45-
getAttributes: vi.fn().mockReturnValue({}),
46-
fetchQualifiedSegments: vi.fn().mockResolvedValue(true),
47-
decide: vi.fn().mockReturnValue(MOCK_DECISION),
48-
decideAll: vi.fn(),
49-
decideForKeys: vi.fn(),
50-
setForcedDecision: vi.fn().mockReturnValue(true),
51-
getForcedDecision: vi.fn(),
52-
removeForcedDecision: vi.fn().mockReturnValue(true),
53-
removeAllForcedDecisions: vi.fn().mockReturnValue(true),
54-
trackEvent: vi.fn(),
55-
getOptimizely: vi.fn(),
56-
setQualifiedSegments: vi.fn(),
57-
getQualifiedSegments: vi.fn().mockReturnValue([]),
58-
qualifiedSegments: null,
59-
...overrides,
60-
} as unknown as OptimizelyUserContext;
61-
}
62-
63-
function createMockClient(hasConfig = false): Client {
64-
return {
65-
getOptimizelyConfig: vi.fn().mockReturnValue(hasConfig ? { revision: '1' } : null),
66-
createUserContext: vi.fn(),
67-
onReady: vi.fn().mockResolvedValue({ success: true }),
68-
notificationCenter: {},
69-
} as unknown as Client;
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-
115-
function createWrapper(store: ProviderStateStore, client: Client) {
116-
const contextValue: OptimizelyContextValue = { store, client };
117-
118-
return function Wrapper({ children }: { children: React.ReactNode }) {
119-
return <OptimizelyContext.Provider value={contextValue}>{children}</OptimizelyContext.Provider>;
120-
};
121-
}
22+
import {
23+
MOCK_DECISION,
24+
createMockUserContext,
25+
createMockClient,
26+
createProviderWrapper,
27+
createWrapper,
28+
} from './testUtils';
29+
import type { OptimizelyDecision, Client, OptimizelyDecideOption } from '@optimizely/optimizely-sdk';
12230

12331
describe('useDecide', () => {
12432
let store: ProviderStateStore;

0 commit comments

Comments
 (0)