Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FSSDK-10544] Refactor Hooks code + tests #273

Merged
merged 6 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 58 additions & 91 deletions src/hooks.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { render, renderHook, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';

import { OptimizelyProvider } from './Provider';
import { OnReadyResult, ReactSDKClient, VariableValuesObject } from './client';
import { NotReadyReason, OnReadyResult, ReactSDKClient, VariableValuesObject } from './client';
import { useExperiment, useFeature, useDecision, useTrackEvent, hooksLogger } from './hooks';
import { OptimizelyDecision } from './utils';
const defaultDecision: OptimizelyDecision = {
Expand Down Expand Up @@ -84,21 +84,38 @@ describe('hooks', () => {
const REJECTION_REASON = 'A rejection reason you should never see in the test runner';

beforeEach(() => {
getOnReadyPromise = ({ timeout = 0 }: any): Promise<OnReadyResult> =>
new Promise((resolve) => {
setTimeout(function () {
resolve(
Object.assign(
{
success: readySuccess,
},
!readySuccess && {
dataReadyPromise: new Promise((r) => setTimeout(r, mockDelay)),
}
)
);
}, timeout || mockDelay);
getOnReadyPromise = ({ timeout = 0 }: any): Promise<OnReadyResult> => {
const timeoutPromise = new Promise<OnReadyResult>((resolve) => {
setTimeout(
() => {
resolve({
success: false,
reason: NotReadyReason.TIMEOUT,
dataReadyPromise: new Promise((r) =>
setTimeout(
() =>
r({
success: readySuccess,
}),
mockDelay
)
),
});
},
timeout || mockDelay + 1
);
});

const clientAndUserReadyPromise = new Promise<OnReadyResult>((resolve) => {
setTimeout(() => {
resolve({
success: readySuccess,
});
}, mockDelay);
});

return Promise.race([clientAndUserReadyPromise, timeoutPromise]);
};
activateMock = jest.fn();
isFeatureEnabledMock = jest.fn();
featureVariables = mockFeatureVariables;
Expand Down Expand Up @@ -176,52 +193,47 @@ describe('hooks', () => {
it('should return a variation when activate returns a variation', async () => {
activateMock.mockReturnValue('12345');

const { container } = render(
render(
<OptimizelyProvider optimizely={optimizelyMock}>
<MyExperimentComponent />
</OptimizelyProvider>
);
await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('12345|true|false'));
});

it('should return null when activate returns null', async () => {
activateMock.mockReturnValue(null);
featureVariables = {};
const { container } = render(
render(
<OptimizelyProvider optimizely={optimizelyMock}>
<MyExperimentComponent />
</OptimizelyProvider>
);
await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('null|true|false'));
});

it('should respect the timeout option passed', async () => {
activateMock.mockReturnValue(null);
mockDelay = 100;
readySuccess = false;

render(
<OptimizelyProvider optimizely={optimizelyMock}>
<MyExperimentComponent options={{ timeout: mockDelay }} />
<MyExperimentComponent options={{ timeout: mockDelay - 10 }} />
</OptimizelyProvider>
);

await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('null|false|false')); // initial render

await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('null|false|true')); // when didTimeout

// Simulate datafile fetch completing after timeout has already passed
// Activate now returns a variation
expect(screen.getByTestId('result')).toHaveTextContent('null|false|false'); // initial render
readySuccess = true;
activateMock.mockReturnValue('12345');
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('12345|true|true')); // when clientReady
// When timeout is reached, but dataReadyPromise is resolved later with the variation
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('12345|true|true'));
});

it('should gracefully handle the client promise rejecting after timeout', async () => {
jest.useFakeTimers();

readySuccess = false;
activateMock.mockReturnValue('12345');

getOnReadyPromise = (): Promise<void> =>
new Promise((_, rej) => setTimeout(() => rej(REJECTION_REASON), mockDelay));

Expand All @@ -231,11 +243,7 @@ describe('hooks', () => {
</OptimizelyProvider>
);

jest.advanceTimersByTime(mockDelay + 1);

await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('null|false|false'));

jest.useRealTimers();
});

it('should re-render when the user attributes change using autoUpdate', async () => {
Expand All @@ -249,16 +257,14 @@ describe('hooks', () => {

// TODO - Wrap this with async act() once we upgrade to React 16.9
// See https://github.com/facebook/react/issues/15379
await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('null|true|false'));

activateMock.mockReturnValue('12345');
// Simulate the user object changing
await act(async () => {
userUpdateCallbacks.forEach((fn) => fn());
});
// component.update();
// await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('12345|true|false');

await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('12345|true|false'));
});

Expand All @@ -272,7 +278,6 @@ describe('hooks', () => {

// // TODO - Wrap this with async act() once we upgrade to React 16.9
// // See https://github.com/facebook/react/issues/15379
await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('null|true|false'));

activateMock.mockReturnValue('12345');
Expand Down Expand Up @@ -434,7 +439,6 @@ describe('hooks', () => {
<MyFeatureComponent />
</OptimizelyProvider>
);
await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('true|{"foo":"bar"}|true|false'));
});

Expand All @@ -447,44 +451,33 @@ describe('hooks', () => {
<MyFeatureComponent />
</OptimizelyProvider>
);
await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|true|false'));
});

it('should respect the timeout option passed', async () => {
mockDelay = 100;
isFeatureEnabledMock.mockReturnValue(false);
featureVariables = {};
readySuccess = false;

render(
<OptimizelyProvider optimizely={optimizelyMock}>
<MyFeatureComponent options={{ timeout: mockDelay }} />
<MyFeatureComponent options={{ timeout: mockDelay - 10 }} />
</OptimizelyProvider>
);

await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false')); // initial render

await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|true')); // when didTimeout
// Initial render
expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false');

// Simulate datafile fetch completing after timeout has already passed
// isFeatureEnabled now returns true, getFeatureVariables returns variable values
readySuccess = true;
isFeatureEnabledMock.mockReturnValue(true);
featureVariables = mockFeatureVariables;
// Wait for completion of dataReadyPromise
await optimizelyMock.onReady().then((res) => res.dataReadyPromise);

// Simulate datafile fetch completing after timeout has already passed
// Activate now returns a variation
activateMock.mockReturnValue('12345');
// Wait for completion of dataReadyPromise
await optimizelyMock.onReady().then((res) => res.dataReadyPromise);
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('true|{"foo":"bar"}|true|true')); // when clientReady
// When timeout is reached, but dataReadyPromise is resolved later with the feature enabled
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('true|{"foo":"bar"}|true|true'));
});

it('should gracefully handle the client promise rejecting after timeout', async () => {
jest.useFakeTimers();

readySuccess = false;
isFeatureEnabledMock.mockReturnValue(true);
getOnReadyPromise = (): Promise<void> =>
Expand All @@ -496,11 +489,7 @@ describe('hooks', () => {
</OptimizelyProvider>
);

jest.advanceTimersByTime(mockDelay + 1);

await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false'));

jest.useRealTimers();
});

it('should re-render when the user attributes change using autoUpdate', async () => {
Expand All @@ -515,7 +504,6 @@ describe('hooks', () => {

// TODO - Wrap this with async act() once we upgrade to React 16.9
// See https://github.com/facebook/react/issues/15379
await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|true|false'));

isFeatureEnabledMock.mockReturnValue(true);
Expand All @@ -539,7 +527,6 @@ describe('hooks', () => {

// TODO - Wrap this with async act() once we upgrade to React 16.9
// See https://github.com/facebook/react/issues/15379
await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|true|false'));

isFeatureEnabledMock.mockReturnValue(true);
Expand Down Expand Up @@ -688,8 +675,6 @@ describe('hooks', () => {
<MyDecideComponent />
</OptimizelyProvider>
);
await optimizelyMock.onReady();
// component.update();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('true|{"foo":"bar"}|true|false'));
});

Expand All @@ -699,50 +684,41 @@ describe('hooks', () => {
enabled: false,
variables: { foo: 'bar' },
});

render(
<OptimizelyProvider optimizely={optimizelyMock}>
<MyDecideComponent />
</OptimizelyProvider>
);
await optimizelyMock.onReady();

await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{"foo":"bar"}|true|false'));
});

it('should respect the timeout option passed', async () => {
decideMock.mockReturnValue({ ...defaultDecision });
readySuccess = false;
mockDelay = 100;

render(
<OptimizelyProvider optimizely={optimizelyMock}>
<MyDecideComponent options={{ timeout: mockDelay }} />
<MyDecideComponent options={{ timeout: mockDelay - 10 }} />
</OptimizelyProvider>
);
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false'));

await optimizelyMock.onReady();
// component.update();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|true'));
// Initial render
expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false');

// Simulate datafile fetch completing after timeout has already passed
// flag is now true and decision contains variables
decideMock.mockReturnValue({
...defaultDecision,
enabled: true,
variables: { foo: 'bar' },
});

await optimizelyMock.onReady().then((res) => res.dataReadyPromise);

// Simulate datafile fetch completing after timeout has already passed
// Wait for completion of dataReadyPromise
await optimizelyMock.onReady().then((res) => res.dataReadyPromise);

await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('true|{"foo":"bar"}|true|true')); // when clientReady
readySuccess = true;
// When timeout is reached, but dataReadyPromise is resolved later with the decision value
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('true|{"foo":"bar"}|true|true'));
});

it('should gracefully handle the client promise rejecting after timeout', async () => {
jest.useFakeTimers();

readySuccess = false;
decideMock.mockReturnValue({ ...defaultDecision });
getOnReadyPromise = (): Promise<void> =>
Expand All @@ -754,11 +730,7 @@ describe('hooks', () => {
</OptimizelyProvider>
);

jest.advanceTimersByTime(mockDelay + 1);

await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false'));

jest.useRealTimers();
});

it('should re-render when the user attributes change using autoUpdate', async () => {
Expand All @@ -771,7 +743,6 @@ describe('hooks', () => {

// TODO - Wrap this with async act() once we upgrade to React 16.9
// See https://github.com/facebook/react/issues/15379
await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|true|false'));

decideMock.mockReturnValue({
Expand All @@ -796,7 +767,6 @@ describe('hooks', () => {

// TODO - Wrap this with async act() once we upgrade to React 16.9
// See https://github.com/facebook/react/issues/15379
await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|true|false'));

decideMock.mockReturnValue({
Expand Down Expand Up @@ -990,7 +960,6 @@ describe('hooks', () => {
);

decideMock.mockReturnValue({ ...defaultDecision, variables: { foo: 'bar' } });
await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{"foo":"bar"}|true|false'));
});

Expand All @@ -1016,7 +985,6 @@ describe('hooks', () => {
{ variationKey: 'var2' }
);

await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|true|false'));
});

Expand Down Expand Up @@ -1044,7 +1012,6 @@ describe('hooks', () => {
{ variationKey: 'var2' }
);

await optimizelyMock.onReady();
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|true|false'));
});
});
Expand Down
Loading
Loading