Skip to content

Commit 3e28e68

Browse files
[FSSDK-10616] hooks improvement
1 parent 6daf36f commit 3e28e68

File tree

1 file changed

+221
-14
lines changed

1 file changed

+221
-14
lines changed

src/hooks.spec.tsx

Lines changed: 221 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,32 @@
1515
*/
1616

1717
/// <reference types="jest" />
18-
1918
import * as React from 'react';
2019
import { act } from 'react-dom/test-utils';
2120
import { render, renderHook, screen, waitFor } from '@testing-library/react';
2221
import '@testing-library/jest-dom';
2322

2423
import { OptimizelyProvider } from './Provider';
2524
import { NotReadyReason, OnReadyResult, ReactSDKClient, VariableValuesObject } from './client';
26-
import { useExperiment, useFeature, useDecision, useTrackEvent, hooksLogger } from './hooks';
25+
import { useExperiment, useFeature, useDecision, useTrackEvent } from './hooks';
2726
import { OptimizelyDecision } from './utils';
27+
import { getLogger } from '@optimizely/optimizely-sdk';
28+
29+
jest.mock('@optimizely/optimizely-sdk', () => {
30+
const originalModule = jest.requireActual('@optimizely/optimizely-sdk');
31+
return {
32+
...originalModule,
33+
getLogger: jest.fn().mockReturnValue({
34+
error: jest.fn(),
35+
warn: jest.fn(),
36+
info: jest.fn(),
37+
debug: jest.fn(),
38+
}),
39+
};
40+
});
41+
42+
const hooksLogger = getLogger('ReactSDK');
43+
2844
const defaultDecision: OptimizelyDecision = {
2945
enabled: false,
3046
variables: {},
@@ -79,7 +95,6 @@ describe('hooks', () => {
7995
let forcedVariationUpdateCallbacks: Array<() => void>;
8096
let decideMock: jest.Mock<OptimizelyDecision>;
8197
let setForcedDecisionMock: jest.Mock<void>;
82-
let hooksLoggerErrorSpy: jest.SpyInstance;
8398
const REJECTION_REASON = 'A rejection reason you should never see in the test runner';
8499

85100
beforeEach(() => {
@@ -125,7 +140,6 @@ describe('hooks', () => {
125140
forcedVariationUpdateCallbacks = [];
126141
decideMock = jest.fn();
127142
setForcedDecisionMock = jest.fn();
128-
hooksLoggerErrorSpy = jest.spyOn(hooksLogger, 'error');
129143
optimizelyMock = {
130144
activate: activateMock,
131145
onReady: jest.fn().mockImplementation((config) => getOnReadyPromise(config || {})),
@@ -185,7 +199,7 @@ describe('hooks', () => {
185199
(res) => res.dataReadyPromise,
186200
(err) => null
187201
);
188-
hooksLoggerErrorSpy.mockReset();
202+
jest.resetAllMocks();
189203
});
190204

191205
describe('useExperiment', () => {
@@ -426,6 +440,16 @@ describe('hooks', () => {
426440
});
427441

428442
describe('useFeature', () => {
443+
it('should print error if optimizely is not provided', async () => {
444+
render(
445+
// @ts-ignore
446+
<OptimizelyProvider optimizely={null}>
447+
<MyFeatureComponent />
448+
</OptimizelyProvider>
449+
);
450+
await waitFor(() => expect(hooksLogger.error).toHaveBeenCalled());
451+
});
452+
429453
it('should render true when the feature is enabled', async () => {
430454
isFeatureEnabledMock.mockReturnValue(true);
431455

@@ -655,6 +679,160 @@ describe('hooks', () => {
655679
});
656680

657681
describe('useDecision', () => {
682+
it('should handle no client promise response', async () => {
683+
getOnReadyPromise = () =>
684+
new Promise((resolve) => {
685+
setTimeout(() => {
686+
resolve({
687+
success: false,
688+
reason: NotReadyReason.NO_CLIENT,
689+
dataReadyPromise: new Promise((r) => setTimeout(() => r({ success: false }), mockDelay)),
690+
});
691+
});
692+
});
693+
decideMock.mockReturnValue({ ...defaultDecision });
694+
695+
render(
696+
<OptimizelyProvider optimizely={optimizelyMock}>
697+
<MyDecideComponent />
698+
</OptimizelyProvider>
699+
);
700+
701+
await waitFor(() => {
702+
expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false');
703+
expect(hooksLogger.warn).toHaveBeenCalled();
704+
});
705+
});
706+
707+
it('should handle no client, but data ready promise success', () => {
708+
getOnReadyPromise = () =>
709+
new Promise((resolve) => {
710+
setTimeout(() => {
711+
resolve({
712+
success: false,
713+
reason: NotReadyReason.NO_CLIENT,
714+
dataReadyPromise: new Promise((r) => setTimeout(() => r({ success: true }), mockDelay)),
715+
});
716+
});
717+
});
718+
decideMock.mockReturnValue({ ...defaultDecision });
719+
720+
render(
721+
<OptimizelyProvider optimizely={optimizelyMock}>
722+
<MyDecideComponent />
723+
</OptimizelyProvider>
724+
);
725+
726+
expect(screen.getByTestId('result')).toHaveTextContent('false|{}|true|false');
727+
});
728+
729+
it('should handle user not ready promise response', async () => {
730+
getOnReadyPromise = () =>
731+
new Promise((resolve) => {
732+
setTimeout(() => {
733+
resolve({
734+
success: false,
735+
reason: NotReadyReason.USER_NOT_READY,
736+
dataReadyPromise: new Promise((r) => setTimeout(() => r({ success: false }), mockDelay)),
737+
});
738+
});
739+
});
740+
decideMock.mockReturnValue({ ...defaultDecision });
741+
742+
render(
743+
<OptimizelyProvider optimizely={optimizelyMock}>
744+
<MyDecideComponent />
745+
</OptimizelyProvider>
746+
);
747+
748+
await waitFor(() => {
749+
expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false');
750+
expect(hooksLogger.warn).toHaveBeenCalled();
751+
});
752+
});
753+
754+
it('should handle user not ready, but data ready promise success', () => {
755+
getOnReadyPromise = () =>
756+
new Promise((resolve) => {
757+
setTimeout(() => {
758+
resolve({
759+
success: false,
760+
reason: NotReadyReason.USER_NOT_READY,
761+
dataReadyPromise: new Promise((r) => setTimeout(() => r({ success: true }), mockDelay)),
762+
});
763+
});
764+
});
765+
decideMock.mockReturnValue({ ...defaultDecision });
766+
767+
render(
768+
<OptimizelyProvider optimizely={optimizelyMock}>
769+
<MyDecideComponent />
770+
</OptimizelyProvider>
771+
);
772+
773+
expect(screen.getByTestId('result')).toHaveTextContent('false|{}|true|false');
774+
});
775+
776+
it('should handle default success false case', async () => {
777+
getOnReadyPromise = () =>
778+
new Promise((resolve) => {
779+
setTimeout(() => {
780+
resolve({
781+
success: false,
782+
reason: 'UNKNOWN',
783+
dataReadyPromise: new Promise((r) => setTimeout(() => r({ success: false }), mockDelay)),
784+
});
785+
});
786+
});
787+
decideMock.mockReturnValue({ ...defaultDecision });
788+
789+
render(
790+
<OptimizelyProvider optimizely={optimizelyMock}>
791+
<MyDecideComponent />
792+
</OptimizelyProvider>
793+
);
794+
795+
await waitFor(() => {
796+
expect(hooksLogger.warn).toHaveBeenCalled();
797+
expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false');
798+
});
799+
});
800+
801+
it('should handle default success true case', async () => {
802+
getOnReadyPromise = () =>
803+
new Promise((resolve) => {
804+
setTimeout(() => {
805+
resolve({
806+
success: false,
807+
reason: 'UNKNOWN',
808+
dataReadyPromise: new Promise((r) => setTimeout(() => r({ success: true }), mockDelay)),
809+
});
810+
});
811+
});
812+
decideMock.mockReturnValue({ ...defaultDecision });
813+
814+
render(
815+
<OptimizelyProvider optimizely={optimizelyMock}>
816+
<MyDecideComponent />
817+
</OptimizelyProvider>
818+
);
819+
820+
await waitFor(() => {
821+
expect(hooksLogger.warn).toHaveBeenCalled();
822+
expect(screen.getByTestId('result')).toHaveTextContent('false|{}|true|false');
823+
});
824+
});
825+
826+
it('should print error if optimizely is not provided', async () => {
827+
render(
828+
// @ts-ignore
829+
<OptimizelyProvider optimizely={null}>
830+
<MyDecideComponent />
831+
</OptimizelyProvider>
832+
);
833+
await waitFor(() => expect(hooksLogger.error).toHaveBeenCalled());
834+
});
835+
658836
it('should render true when the flag is enabled', async () => {
659837
decideMock.mockReturnValue({
660838
...defaultDecision,
@@ -705,10 +883,34 @@ describe('hooks', () => {
705883
variables: { foo: 'bar' },
706884
});
707885
readySuccess = true;
886+
708887
// When timeout is reached, but dataReadyPromise is resolved later with the decision value
709888
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('true|{"foo":"bar"}|true|true'));
710889
});
711890

891+
it('should log warn message if dataReadyPromise resolves as false', async () => {
892+
decideMock.mockReturnValue({ ...defaultDecision });
893+
readySuccess = false;
894+
mockDelay = 20;
895+
896+
render(
897+
<OptimizelyProvider optimizely={optimizelyMock}>
898+
<MyDecideComponent options={{ timeout: mockDelay - 10 }} />
899+
</OptimizelyProvider>
900+
);
901+
902+
// Initial render
903+
expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false');
904+
905+
readySuccess = false;
906+
907+
// When timeout is reached, but dataReadyPromise is resolved later with the decision value
908+
await waitFor(() => {
909+
expect(hooksLogger.warn).toHaveBeenCalled();
910+
expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|true');
911+
});
912+
});
913+
712914
it('should gracefully handle the client promise rejecting after timeout', async () => {
713915
readySuccess = false;
714916
decideMock.mockReturnValue({ ...defaultDecision });
@@ -721,7 +923,9 @@ describe('hooks', () => {
721923
</OptimizelyProvider>
722924
);
723925

724-
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false'));
926+
await waitFor(() => {
927+
expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false');
928+
});
725929
});
726930

727931
it('should re-render when the user attributes change using autoUpdate', async () => {
@@ -1002,16 +1206,17 @@ describe('hooks', () => {
10021206
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|true|false'));
10031207
});
10041208
});
1209+
10051210
describe('useTrackEvent', () => {
10061211
it('returns track method and false states when optimizely is not provided', () => {
10071212
const wrapper = ({ children }: { children: React.ReactNode }): React.ReactElement => {
10081213
//@ts-ignore
10091214
return <OptimizelyProvider>{children}</OptimizelyProvider>;
10101215
};
1216+
10111217
const { result } = renderHook(() => useTrackEvent(), { wrapper });
1012-
expect(result.current[0]).toBeInstanceOf(Function);
1013-
expect(result.current[1]).toBe(false);
1014-
expect(result.current[2]).toBe(false);
1218+
1219+
expect(result.current).toEqual([expect.any(Function), false, false]);
10151220
});
10161221

10171222
it('returns track method along with clientReady and didTimeout state when optimizely instance is provided', () => {
@@ -1022,9 +1227,10 @@ describe('hooks', () => {
10221227
);
10231228

10241229
const { result } = renderHook(() => useTrackEvent(), { wrapper });
1025-
expect(result.current[0]).toBeInstanceOf(Function);
1026-
expect(typeof result.current[1]).toBe('boolean');
1027-
expect(typeof result.current[2]).toBe('boolean');
1230+
result.current[0]('eventKey');
1231+
1232+
expect(optimizelyMock.track).toHaveBeenCalledTimes(1);
1233+
expect(result.current).toEqual([expect.any(Function), true, false]);
10281234
});
10291235

10301236
it('Log error when track method is called and optimizely instance is not provided', () => {
@@ -1034,7 +1240,7 @@ describe('hooks', () => {
10341240
};
10351241
const { result } = renderHook(() => useTrackEvent(), { wrapper });
10361242
result.current[0]('eventKey');
1037-
expect(hooksLogger.error).toHaveBeenCalledTimes(1);
1243+
expect(hooksLogger.error).toHaveBeenCalled();
10381244
});
10391245

10401246
it('Log error when track method is called and client is not ready', () => {
@@ -1048,7 +1254,8 @@ describe('hooks', () => {
10481254

10491255
const { result } = renderHook(() => useTrackEvent(), { wrapper });
10501256
result.current[0]('eventKey');
1051-
expect(hooksLogger.error).toHaveBeenCalledTimes(1);
1257+
1258+
expect(hooksLogger.error).toHaveBeenCalled();
10521259
});
10531260
});
10541261
});

0 commit comments

Comments
 (0)