diff --git a/src/Experiment.spec.tsx b/src/Experiment.spec.tsx index dab0ff2..1f5089b 100644 --- a/src/Experiment.spec.tsx +++ b/src/Experiment.spec.tsx @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + /// + import * as React from 'react'; import { act } from 'react-dom/test-utils'; import { render, screen, waitFor } from '@testing-library/react'; @@ -43,10 +45,11 @@ describe('', () => { optimizelyMock = ({ onReady: jest.fn().mockImplementation(() => onReadyPromise), activate: jest.fn().mockImplementation(() => variationKey), - onUserUpdate: jest.fn().mockImplementation(() => () => { }), + onUserUpdate: jest.fn().mockImplementation(() => () => {}), + getVuid: jest.fn().mockImplementation(() => 'vuid_95bf72cebc774dfd8e8e580a5a1'), notificationCenter: { - addNotificationListener: jest.fn().mockImplementation(() => { }), - removeNotificationListener: jest.fn().mockImplementation(() => { }), + addNotificationListener: jest.fn().mockImplementation(() => {}), + removeNotificationListener: jest.fn().mockImplementation(() => {}), }, user: { id: 'testuser', @@ -55,7 +58,7 @@ describe('', () => { isReady: jest.fn().mockImplementation(() => isReady), getIsReadyPromiseFulfilled: () => true, getIsUsingSdkKey: () => true, - onForcedVariationsUpdate: jest.fn().mockReturnValue(() => { }), + onForcedVariationsUpdate: jest.fn().mockReturnValue(() => {}), } as unknown) as ReactSDKClient; }); @@ -405,7 +408,9 @@ describe('', () => { await optimizelyMock.onReady(); expect(optimizelyMock.activate).toHaveBeenCalledWith('experiment1', undefined, undefined); - await waitFor(() => expect(screen.getByTestId('variation-key')).toHaveTextContent('matchingVariation|true|false')); + await waitFor(() => + expect(screen.getByTestId('variation-key')).toHaveTextContent('matchingVariation|true|false') + ); }); describe('when the onReady() promise return { success: false }', () => { diff --git a/src/Feature.spec.tsx b/src/Feature.spec.tsx index bf667ad..a747e80 100644 --- a/src/Feature.spec.tsx +++ b/src/Feature.spec.tsx @@ -1,11 +1,11 @@ /** - * Copyright 2018-2019, Optimizely + * Copyright 2018-2019, 2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/// + import * as React from 'react'; import { act } from 'react-dom/test-utils'; import { render, screen, waitFor } from '@testing-library/react'; @@ -45,6 +48,7 @@ describe('', () => { getFeatureVariables: jest.fn().mockImplementation(() => featureVariables), isFeatureEnabled: jest.fn().mockImplementation(() => isEnabledMock), onUserUpdate: jest.fn().mockImplementation(handler => () => {}), + getVuid: jest.fn().mockImplementation(() => 'vuid_95bf72cebc774dfd8e8e580a5a1'), notificationCenter: { addNotificationListener: jest.fn().mockImplementation((type, handler) => {}), removeNotificationListener: jest.fn().mockImplementation(id => {}), diff --git a/src/Provider.tsx b/src/Provider.tsx index af9c15d..17b7fb3 100644 --- a/src/Provider.tsx +++ b/src/Provider.tsx @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import * as React from 'react'; import { UserAttributes } from '@optimizely/optimizely-sdk'; import { getLogger } from '@optimizely/optimizely-sdk'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { OptimizelyContextProvider } from './Context'; -import { ReactSDKClient } from './client'; +import { ReactSDKClient, DefaultUser } from './client'; import { areUsersEqual, UserInfo } from './utils'; const logger = getLogger(''); @@ -42,9 +43,20 @@ interface OptimizelyProviderState { export class OptimizelyProvider extends React.Component { constructor(props: OptimizelyProviderProps) { super(props); - const { optimizely, userId, userAttributes, user } = props; + } + + componentDidMount(): void { + this.setUserInOptimizely(); + } + + async setUserInOptimizely(): Promise { + const { optimizely, userId, userAttributes, user } = this.props; + + if (!optimizely) { + logger.error('OptimizelyProvider must be passed an instance of the Optimizely SDK client'); + return; + } - // check if user id/attributes are provided as props and set them ReactSDKClient let finalUser: UserInfo | null = null; if (user) { @@ -65,17 +77,16 @@ export class OptimizelyProvider extends React.Component { return { logger: { - warn: jest.fn(() => () => { }), - info: jest.fn(() => () => { }), - error: jest.fn(() => () => { }), - debug: jest.fn(() => () => { }), + warn: jest.fn(() => () => {}), + info: jest.fn(() => () => {}), + error: jest.fn(() => () => {}), + debug: jest.fn(() => () => {}), }, }; }); @@ -1236,10 +1237,6 @@ describe('ReactSDKClient', () => { const result = await instance.fetchQualifiedSegments(); expect(result).toEqual(false); - expect(logger.warn).toHaveBeenCalledTimes(1); - expect(logger.warn).toBeCalledWith( - 'Unable to fetch qualified segments for user because Optimizely client failed to initialize.' - ); }); it('should return false if fetch fails', async () => { @@ -1668,15 +1665,14 @@ describe('ReactSDKClient', () => { instance.getUserContext(); expect(logger.warn).toHaveBeenCalledTimes(1); - expect(logger.warn).toBeCalledWith("Unable to get user context because Optimizely client failed to initialize."); + expect(logger.warn).toBeCalledWith('Unable to get user context. Optimizely client not initialized.'); }); - it('should log a warning and return null if setUser is not called first', () => { instance.getUserContext(); expect(logger.warn).toHaveBeenCalledTimes(1); - expect(logger.warn).toBeCalledWith("Unable to get user context because user was not set."); + expect(logger.warn).toBeCalledWith('Unable to get user context. User context not set.'); }); it('should return a userContext if setUser is called', () => { diff --git a/src/client.ts b/src/client.ts index a986d1b..20c29cf 100644 --- a/src/client.ts +++ b/src/client.ts @@ -44,7 +44,7 @@ export interface OnReadyResult extends ResolveResult { const REACT_SDK_CLIENT_ENGINE = 'react-sdk'; const REACT_SDK_CLIENT_VERSION = '3.0.0-beta2'; -const default_user: UserInfo = { +export const DefaultUser: UserInfo = { id: null, attributes: {}, }; @@ -52,7 +52,7 @@ const default_user: UserInfo = { export interface ReactSDKClient extends Omit { user: UserInfo; - onReady(opts?: { timeout?: number; }): Promise; + onReady(opts?: { timeout?: number }): Promise; setUser(userInfo: UserInfo): Promise; onUserUpdate(handler: OnUserUpdateHandler): DisposeFn; isReady(): boolean; @@ -123,7 +123,7 @@ export interface ReactSDKClient extends Omit void; - private userPromise: Promise; - private isUserPromiseResolved = false; private onUserUpdateHandlers: OnUserUpdateHandler[] = []; private onForcedVariationsUpdateHandlers: OnForcedVariationsUpdateHandler[] = []; private forcedDecisionFlagKeys: Set = new Set(); @@ -203,10 +200,6 @@ class OptimizelyReactSDKClient implements ReactSDKClient { // We need to add autoupdate listener to the hooks after the instance became fully ready to avoid redundant updates to hooks private isReadyPromiseFulfilled = false; - // Its usually true from the beginning when user is provided as an object in the `OptimizelyProvider` - // This becomes more significant when a promise is provided instead. - private isUserReady = false; - private isUsingSdkKey = false; private readonly _client: optimizely.Client | null; @@ -215,7 +208,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { private dataReadyPromise: Promise; public initialConfig: optimizely.Config; - public user: UserInfo = { ...default_user }; + public user: UserInfo = { ...DefaultUser }; /** * Creates an instance of OptimizelyReactSDKClient. @@ -223,7 +216,6 @@ class OptimizelyReactSDKClient implements ReactSDKClient { */ constructor(config: optimizely.Config) { this.initialConfig = config; - this.userPromiseResolver = () => { }; const configWithClientInfo = { ...config, @@ -235,31 +227,18 @@ class OptimizelyReactSDKClient implements ReactSDKClient { this.isClientReady = !!configWithClientInfo.datafile; this.isUsingSdkKey = !!configWithClientInfo.sdkKey; - this.userPromise = new Promise(resolve => { - this.userPromiseResolver = resolve; - }).then((result: ResolveResult) => { - this.isUserReady = result.success; - return result; - }); - if (this._client) { - this._client.onReady().then(() => { + this.dataReadyPromise = this._client.onReady().then((clientResult: { success: boolean }) => { + this.isReadyPromiseFulfilled = true; this.isClientReady = true; - }); - - this.dataReadyPromise = Promise.all([this.userPromise, this._client.onReady()]).then( - ([userResult, clientResult]) => { - this.isReadyPromiseFulfilled = true; - const bothSuccessful = userResult.success && clientResult.success; - return { - success: true, // bothSuccessful, - message: bothSuccessful - ? 'Successfully resolved user information and client datafile.' - : 'User information or client datafile was not not ready.', - }; - } - ); + return { + success: true, + message: clientResult.success + ? 'Successfully resolved client datafile.' + : 'Client datafile was not not ready.', + }; + }); } else { logger.warn('Unable to resolve datafile and user information because Optimizely client failed to initialize.'); @@ -273,17 +252,14 @@ class OptimizelyReactSDKClient implements ReactSDKClient { } } - protected getUserContextWithOverrides( - overrideUserId?: string, - overrideAttributes?: optimizely.UserAttributes - ): UserInfo { - const finalUserId: string | null = overrideUserId === undefined ? this.user.id : overrideUserId; - const finalUserAttributes: optimizely.UserAttributes | undefined = + protected getUserWithOverrides(overrideUserId?: string, overrideAttributes?: optimizely.UserAttributes): UserInfo { + const id: string | null = overrideUserId === undefined ? this.user.id : overrideUserId; + const attributes: optimizely.UserAttributes | undefined = overrideAttributes === undefined ? this.user.attributes : overrideAttributes; return { - id: finalUserId, - attributes: finalUserAttributes, + id, + attributes, }; } @@ -295,7 +271,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return this.isUsingSdkKey; } - public onReady(config: { timeout?: number; } = {}): Promise { + public onReady(config: { timeout?: number } = {}): Promise { let timeoutId: number | undefined; let timeout: number = DEFAULT_ON_READY_TIMEOUT; if (config && config.timeout !== undefined) { @@ -307,8 +283,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { resolve({ success: false, reason: 'TIMEOUT', - message: - 'failed to initialize onReady before timeout, either the datafile or user info was not set before the timeout', + message: 'Failed to initialize onReady before timeout, data was not set before the timeout period', dataReadyPromise: this.dataReadyPromise, }); }, timeout) as any; @@ -332,57 +307,42 @@ class OptimizelyReactSDKClient implements ReactSDKClient { public getUserContext(): optimizely.OptimizelyUserContext | null { if (!this._client) { - logger.warn( - 'Unable to get user context because Optimizely client failed to initialize.' - ); + logger.warn('Unable to get user context. Optimizely client not initialized.'); return null; } if (!this.userContext) { - logger.warn( - 'Unable to get user context because user was not set.' - ); + logger.warn('Unable to get user context. User context not set.'); return null; } return this.userContext; } - public getUserContextInstance(userInfo: UserInfo): optimizely.OptimizelyUserContext | null { + private setCurrentUserContext(userInfo: UserInfo): void { if (!this._client) { - logger.warn( - 'Unable to get user context for user id "%s" because Optimizely client failed to initialize.', - userInfo.id - ); - return null; + logger.warn(`Unable to set user context for user ID ${userInfo.id}. Optimizely client not initialized.`); + return; } - let userContext: optimizely.OptimizelyUserContext | null = null; - - if (this.userContext) { - if (areUsersEqual(userInfo, this.user)) { - return this.userContext; - } - - if (userInfo.id) { - userContext = this._client.createUserContext(userInfo.id, userInfo.attributes); - return userContext; - } - - return null; + if (!this.userContext || (this.userContext && !areUsersEqual(userInfo, this.user))) { + this.userContext = this._client.createUserContext(userInfo.id || undefined, userInfo.attributes); } + } - if (userInfo.id) { - this.userContext = this._client.createUserContext(userInfo.id, userInfo.attributes); - return this.userContext; + private makeUserContextInstance(userInfo: UserInfo): optimizely.OptimizelyUserContext | null { + if (!this._client || !this.isReady()) { + logger.warn( + `Unable to create user context for ${userInfo.id}. Optimizely client failed to initialize or not ready.` + ); + return null; } - return null; + return this._client.createUserContext(userInfo.id || undefined, userInfo.attributes); } public async fetchQualifiedSegments(options?: optimizely.OptimizelySegmentOption[]): Promise { - if (!this.userContext) { - logger.warn('Unable to fetch qualified segments for user because Optimizely client failed to initialize.'); + if (!this.userContext || !this.isReady()) { return false; } @@ -390,35 +350,17 @@ class OptimizelyReactSDKClient implements ReactSDKClient { } public async setUser(userInfo: UserInfo): Promise { - this.isUserReady = true; - - //reset user info - this.user = { ...default_user }; + this.setCurrentUserContext(userInfo); - this.user.id = userInfo.id; - - if (this._client) { - this.userContext = this._client.createUserContext(userInfo.id ?? undefined, userInfo.attributes); - } else { - logger.warn( - 'Unable to create user context for user id "%s" because Optimizely client failed to initialize.', - this.user.id - ); - } - - if (userInfo.attributes) { - this.user.attributes = userInfo.attributes; - } + this.user = { + id: userInfo.id || DefaultUser.id, + attributes: userInfo.attributes || DefaultUser.attributes, + }; if (this.getIsReadyPromiseFulfilled()) { await this.fetchQualifiedSegments(); } - if (!this.isUserPromiseResolved) { - this.userPromiseResolver({ success: true }); - this.isUserPromiseResolved = true; - } - this.onUserUpdateHandlers.forEach(handler => handler(this.user)); } @@ -451,8 +393,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { } public isReady(): boolean { - // React SDK Instance only becomes ready when both JS SDK client and the user info is ready. - return this.isUserReady && this.isClientReady; + return this.isClientReady; } /** @@ -473,7 +414,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return null; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to activate experiment "%s" because User ID is not set', experimentKey); @@ -498,14 +439,14 @@ class OptimizelyReactSDKClient implements ReactSDKClient { ); } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to evaluate feature "%s" because User ID is not set.', key); return createFailedDecision(key, `Unable to evaluate flag ${key} because User ID is not set.`, user); } - const optlyUserContext = this.getUserContextInstance(user); + const optlyUserContext = this.makeUserContextInstance(user); if (optlyUserContext) { return { ...optlyUserContext.decide(key, options), @@ -523,23 +464,23 @@ class OptimizelyReactSDKClient implements ReactSDKClient { options: optimizely.OptimizelyDecideOption[] = [], overrideUserId?: string, overrideAttributes?: optimizely.UserAttributes - ): { [key: string]: OptimizelyDecision; } { + ): { [key: string]: OptimizelyDecision } { if (!this._client) { logger.warn('Unable to evaluate features for keys because Optimizely client failed to initialize.'); return {}; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to evaluate features for keys because User ID is not set'); return {}; } - const optlyUserContext = this.getUserContextInstance(user); + const optlyUserContext = this.makeUserContextInstance(user); if (optlyUserContext) { return Object.entries(optlyUserContext.decideForKeys(keys, options)).reduce( - (decisions: { [key: string]: OptimizelyDecision; }, [key, decision]) => { + (decisions: { [key: string]: OptimizelyDecision }, [key, decision]) => { decisions[key] = { ...decision, userContext: { @@ -559,23 +500,23 @@ class OptimizelyReactSDKClient implements ReactSDKClient { options: optimizely.OptimizelyDecideOption[] = [], overrideUserId?: string, overrideAttributes?: optimizely.UserAttributes - ): { [key: string]: OptimizelyDecision; } { + ): { [key: string]: OptimizelyDecision } { if (!this._client) { logger.warn('Unable to evaluate all feature decisions because Optimizely client is not initialized.'); return {}; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to evaluate all feature decisions because User ID is not set'); return {}; } - const optlyUserContext = this.getUserContextInstance(user); + const optlyUserContext = this.makeUserContextInstance(user); if (optlyUserContext) { return Object.entries(optlyUserContext.decideAll(options)).reduce( - (decisions: { [key: string]: OptimizelyDecision; }, [key, decision]) => { + (decisions: { [key: string]: OptimizelyDecision }, [key, decision]) => { decisions[key] = { ...decision, userContext: { @@ -612,7 +553,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return null; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to get variation for experiment "%s" because User ID is not set', experimentKey); @@ -647,7 +588,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { overrideAttributes = undefined; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to send tracking event "%s" because User ID is not set', eventKey); @@ -760,7 +701,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return false; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to determine if feature "%s" is enabled because User ID is not set', feature); @@ -797,7 +738,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return {}; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); const userId = user.id; if (userId === null) { @@ -858,7 +799,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return null; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to get feature variable string from feature "%s" because User ID is not set', feature); @@ -892,7 +833,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return null; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to get feature variable boolean from feature "%s" because User ID is not set', feature); @@ -926,7 +867,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return null; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to get feature variable integer from feature "%s" because User ID is not set', feature); @@ -960,7 +901,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return null; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to get feature variable double from feature "%s" because User ID is not set', feature); @@ -994,7 +935,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return null; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to get feature variable JSON from feature "%s" because User ID is not set', feature); @@ -1029,7 +970,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return null; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info( @@ -1055,7 +996,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { featureKey: string, overrideUserId: string, overrideAttributes?: optimizely.UserAttributes - ): { [variableKey: string]: unknown; } | null { + ): { [variableKey: string]: unknown } | null { if (!this._client) { logger.warn( 'Unable to get all feature variables from feature "%s" because Optimizely client failed to initialize.', @@ -1064,7 +1005,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return {}; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to get all feature variables from feature "%s" because User ID is not set', featureKey); @@ -1087,7 +1028,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return []; } - const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + const user = this.getUserWithOverrides(overrideUserId, overrideAttributes); if (user.id === null) { logger.info('Unable to get list of enabled features because User ID is not set'); @@ -1113,7 +1054,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return null; } - const user = this.getUserContextWithOverrides(overrideUserId); + const user = this.getUserWithOverrides(overrideUserId); if (user.id === null) { logger.info('Unable to get forced variation for experiment "%s" because User ID is not set', experiment); @@ -1148,9 +1089,9 @@ class OptimizelyReactSDKClient implements ReactSDKClient { let finalVariationKey: string | null = null; if (arguments.length === 2) { finalVariationKey = overrideUserIdOrVariationKey; - finalUserId = this.getUserContextWithOverrides().id; + finalUserId = this.getUserWithOverrides().id; } else if (arguments.length === 3) { - finalUserId = this.getUserContextWithOverrides(overrideUserIdOrVariationKey).id; + finalUserId = this.getUserWithOverrides(overrideUserIdOrVariationKey).id; if (variationKey === undefined) { // can't have undefined if supplying all 3 arguments return false; @@ -1184,7 +1125,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { * Cleanup method for killing an running timers and flushing eventQueue * @returns {Promise<{ success: boolean; reason?: string }>} */ - public close(): Promise<{ success: boolean; reason?: string; }> { + public close(): Promise<{ success: boolean; reason?: string }> { if (!this._client) { /** * Note: @@ -1193,7 +1134,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { * - If we resolve as "false", then the cleanup for timers and the event queue will never trigger. * - Not triggering cleanup may lead to memory leaks and other inefficiencies. */ - return new Promise<{ success: boolean; reason: string; }>((resolve, reject) => + return new Promise<{ success: boolean; reason: string }>((resolve, reject) => resolve({ success: true, reason: 'Optimizely client is not initialized.', diff --git a/src/hooks.spec.tsx b/src/hooks.spec.tsx index 7a1f580..3615460 100644 --- a/src/hooks.spec.tsx +++ b/src/hooks.spec.tsx @@ -1,11 +1,11 @@ /** - * Copyright 2022, Optimizely + * Copyright 2022, 2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/// + import * as React from 'react'; import { act } from 'react-dom/test-utils'; import { render, screen, waitFor } from '@testing-library/react'; @@ -78,6 +81,8 @@ describe('hooks', () => { let decideMock: jest.Mock; let setForcedDecisionMock: jest.Mock; + const REJECTION_REASON = 'A rejection reason you should never see in the test runner'; + beforeEach(() => { getOnReadyPromise = ({ timeout = 0 }: any): Promise => new Promise(resolve => { @@ -111,6 +116,7 @@ describe('hooks', () => { onReady: jest.fn().mockImplementation(config => getOnReadyPromise(config || {})), getFeatureVariables: jest.fn().mockImplementation(() => featureVariables), isFeatureEnabled: isFeatureEnabledMock, + getVuid: jest.fn().mockReturnValue('vuid_95bf72cebc774dfd8e8e580a5a1'), onUserUpdate: jest.fn().mockImplementation(handler => { userUpdateCallbacks.push(handler); return () => {}; @@ -212,22 +218,24 @@ describe('hooks', () => { }); it('should gracefully handle the client promise rejecting after timeout', async () => { + jest.useFakeTimers(); + readySuccess = false; activateMock.mockReturnValue('12345'); - getOnReadyPromise = () => - new Promise((res, rej) => { - setTimeout(() => rej('some error with user'), mockDelay); - }); + getOnReadyPromise = (): Promise => + new Promise((_, rej) => setTimeout(() => rej(REJECTION_REASON), mockDelay)); render( ); - await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('null|false|false')); // initial render - await new Promise(r => setTimeout(r, mockDelay * 3)); + 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 () => { @@ -475,22 +483,24 @@ describe('hooks', () => { }); it('should gracefully handle the client promise rejecting after timeout', async () => { + jest.useFakeTimers(); + readySuccess = false; isFeatureEnabledMock.mockReturnValue(true); - getOnReadyPromise = () => - new Promise((res, rej) => { - setTimeout(() => rej('some error with user'), mockDelay); - }); + getOnReadyPromise = (): Promise => + new Promise((_, rej) => setTimeout(() => rej(REJECTION_REASON), mockDelay)); render( ); - await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false')); // initial render - await new Promise(r => setTimeout(r, mockDelay * 3)); + 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 () => { @@ -731,20 +741,24 @@ describe('hooks', () => { }); it('should gracefully handle the client promise rejecting after timeout', async () => { + jest.useFakeTimers(); + readySuccess = false; decideMock.mockReturnValue({ ...defaultDecision }); - getOnReadyPromise = () => - new Promise((res, rej) => { - setTimeout(() => rej('some error with user'), mockDelay); - }); + getOnReadyPromise = (): Promise => + new Promise((_, rej) => setTimeout(() => rej(REJECTION_REASON), mockDelay)); + render( ); - await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|false|false')); // initial render - await new Promise(r => setTimeout(r, mockDelay * 3)); + + 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 () => { diff --git a/src/withOptimizely.spec.tsx b/src/withOptimizely.spec.tsx index 3fa8393..80a8be9 100644 --- a/src/withOptimizely.spec.tsx +++ b/src/withOptimizely.spec.tsx @@ -1,11 +1,11 @@ /** - * Copyright 2018-2019, Optimizely + * Copyright 2018-2019, 2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + /// import * as React from 'react'; @@ -34,7 +35,7 @@ class InnerComponent extends React.Component { super(props); } - render() { + render(): JSX.Element { return (
{JSON.stringify({ ...this.props })} @@ -51,11 +52,13 @@ describe('withOptimizely', () => { beforeEach(() => { optimizelyClient = ({ setUser: jest.fn(), + getVuid: jest.fn(), + onReady: jest.fn(), } as unknown) as ReactSDKClient; }); describe('when userId / userAttributes props are provided', () => { - it('should call setUser with the correct user id / attributes', () => { + it('should call setUser with the correct user id / attributes', async () => { const attributes = { foo: 'bar', }; @@ -66,13 +69,13 @@ describe('withOptimizely', () => { ); - expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1); + await waitFor(() => expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1)); expect(optimizelyClient.setUser).toHaveBeenCalledWith({ id: userId, attributes }); }); }); describe('when only userId prop is provided', () => { - it('should call setUser with the correct user id / attributes', () => { + it('should call setUser with the correct user id / attributes', async () => { const userId = 'jordan'; render( @@ -80,7 +83,7 @@ describe('withOptimizely', () => { ); - expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1); + await waitFor(() => expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1)); expect(optimizelyClient.setUser).toHaveBeenCalledWith({ id: userId, attributes: {}, @@ -89,7 +92,7 @@ describe('withOptimizely', () => { }); describe(`when the user prop is passed only with "id"`, () => { - it('should call setUser with the correct user id / attributes', () => { + it('should call setUser with the correct user id / attributes', async () => { const userId = 'jordan'; render( @@ -97,7 +100,7 @@ describe('withOptimizely', () => { ); - expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1); + await waitFor(() => expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1)); expect(optimizelyClient.setUser).toHaveBeenCalledWith({ id: userId, attributes: {}, @@ -106,7 +109,7 @@ describe('withOptimizely', () => { }); describe(`when the user prop is passed with "id" and "attributes"`, () => { - it('should call setUser with the correct user id / attributes', () => { + it('should call setUser with the correct user id / attributes', async () => { const userId = 'jordan'; const attributes = { foo: 'bar' }; render( @@ -115,7 +118,7 @@ describe('withOptimizely', () => { ); - expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1); + await waitFor(() => expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1)); expect(optimizelyClient.setUser).toHaveBeenCalledWith({ id: userId, attributes, @@ -124,7 +127,7 @@ describe('withOptimizely', () => { }); describe('when both the user prop and userId / userAttributes props are passed', () => { - it('should respect the user object prop', () => { + it('should respect the user object prop', async () => { const userId = 'jordan'; const attributes = { foo: 'bar' }; render( @@ -139,7 +142,7 @@ describe('withOptimizely', () => { ); - expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1); + await waitFor(() => expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1)); expect(optimizelyClient.setUser).toHaveBeenCalledWith({ id: userId, attributes, @@ -160,7 +163,7 @@ describe('withOptimizely', () => { ) ); - expect(optimizelyClient.setUser).not.toHaveBeenCalled(); + expect(optimizelyClient.setUser).toHaveBeenCalled(); }); it('should inject the isServerSide prop', async () => { @@ -187,13 +190,9 @@ describe('withOptimizely', () => { const OptimizelyInput = withOptimizely(ForwardingFancyInput); const inputRef: React.RefObject = React.createRef(); - const optimizelyMock: ReactSDKClient = ({ - setUser: jest.fn(), - } as unknown) as ReactSDKClient; - render( { ); + expect(inputRef).toBeDefined(); expect(inputRef.current).toBeInstanceOf(HTMLInputElement); - expect(typeof inputRef.current!.focus).toBe('function'); + expect(typeof inputRef.current?.focus).toBe('function'); const inputNode: HTMLInputElement = screen.getByTestId('input-element'); - expect(inputRef.current!).toBe(inputNode); + expect(inputRef.current).toBe(inputNode); }); it('should hoist non-React statics', () => {