import { GeneralError } from '@openfeature/core'; import type { Client, EvaluationContext, EvaluationDetails, Hook, JsonArray, JsonObject, JsonValue, Provider, ResolutionDetails, TransactionContext, TransactionContextPropagator, } from '../src'; import { ErrorCode, FlagNotFoundError, OpenFeature, ProviderFatalError, ProviderStatus, StandardResolutionReasons, } from '../src'; import { OpenFeatureClient } from '../src/client/internal/open-feature-client'; import { isDeepStrictEqual } from 'node:util'; import type { HookContext } from '@openfeature/core'; import type { TrackingEventDetails } from '@openfeature/core'; const BOOLEAN_VALUE = true; const STRING_VALUE = 'val'; const NUMBER_VALUE = 2048; const ARRAY_VALUE: JsonValue[] = []; const INNER_KEY = 'inner'; const INNER_NULL_KEY = 'nullKey'; const INNER_BOOLEAN_KEY = 'booleanKey'; const INNER_STRING_KEY = 'stringKey'; const INNER_NUMBER_KEY = 'numberKey'; const INNER_ARRAY_KEY = 'arrayKey'; const OBJECT_VALUE: JsonValue = { [INNER_KEY]: { [INNER_NULL_KEY]: null, [INNER_BOOLEAN_KEY]: BOOLEAN_VALUE, [INNER_STRING_KEY]: STRING_VALUE, [INNER_NUMBER_KEY]: NUMBER_VALUE, [INNER_ARRAY_KEY]: ARRAY_VALUE, }, }; const DATETIME_VALUE = new Date(2022, 5, 13, 18, 20, 0); const BOOLEAN_VARIANT = `${BOOLEAN_VALUE}`; const STRING_VARIANT = `${STRING_VALUE}-variant`; const NUMBER_VARIANT = NUMBER_VALUE.toString(); const OBJECT_VARIANT = 'json'; const REASON = 'mocked-value'; // a mock provider with some jest spies const MOCK_PROVIDER: Provider = { metadata: { name: 'mock', }, track: jest.fn((): void => { return; }), resolveBooleanEvaluation: jest.fn((): Promise> => { return Promise.resolve({ value: BOOLEAN_VALUE, variant: BOOLEAN_VARIANT, reason: REASON, }); }), resolveStringEvaluation: jest.fn((): Promise> => { return Promise.resolve({ value: STRING_VALUE, variant: STRING_VARIANT, reason: REASON, }) as Promise>; }) as () => Promise>, resolveNumberEvaluation: jest.fn((): Promise> => { return Promise.resolve({ value: NUMBER_VALUE, variant: NUMBER_VARIANT, reason: REASON, }); }), resolveObjectEvaluation: jest.fn((): Promise> => { const details = Promise.resolve>({ value: OBJECT_VALUE as U, variant: OBJECT_VARIANT, reason: REASON, }); return details as Promise>; }) as () => Promise>, }; describe('OpenFeatureClient', () => { beforeEach(() => { OpenFeature.setProvider(MOCK_PROVIDER); }); afterEach(async () => { await OpenFeature.clearProviders(); jest.clearAllMocks(); }); describe('Requirement 1.1.8', () => { class mockAsyncProvider implements Provider { metadata = { name: 'mock-async', }; status = ProviderStatus.NOT_READY; readonly runsOn = 'server'; constructor(private readonly throwInInit: boolean) {} async initialize(): Promise { if (this.throwInInit) { try { throw new Error('provider failed to initialize'); } catch (err) { this.status = ProviderStatus.ERROR; throw err; } } this.status = ProviderStatus.READY; return; } resolveBooleanEvaluation(): Promise> { throw new Error('Method not implemented.'); } resolveStringEvaluation(): Promise> { throw new Error('Method not implemented.'); } resolveNumberEvaluation(): Promise> { throw new Error('Method not implemented.'); } resolveObjectEvaluation(): Promise> { throw new Error('Method not implemented.'); } } it('should wait for the provider to successfully initialize', async () => { const spy = jest.spyOn(mockAsyncProvider.prototype, 'initialize'); const provider = new mockAsyncProvider(false); expect(provider.status).toBe(ProviderStatus.NOT_READY); await OpenFeature.setProviderAndWait(provider); expect(provider.status).toBe(ProviderStatus.READY); expect(spy).toHaveBeenCalled(); }); it('should wait for the provider to fail during initialization', async () => { const spy = jest.spyOn(mockAsyncProvider.prototype, 'initialize'); const provider = new mockAsyncProvider(true); expect(provider.status).toBe(ProviderStatus.NOT_READY); await expect(OpenFeature.setProviderAndWait(provider)).rejects.toThrow(); expect(provider.status).toBe(ProviderStatus.ERROR); expect(spy).toHaveBeenCalled(); }); }); describe('Requirement 1.2.1', () => { it('should allow addition of hooks', () => { expect(OpenFeatureClient.prototype.addHooks).toBeDefined(); }); }); describe('Requirement 1.2.1', () => { const domain = 'my-domain'; const client = OpenFeature.getClient(domain); it('should have metadata accessor with name for backwards compatibility', () => { expect(client.metadata.name).toEqual(domain); }); it('should have metadata accessor with domain', () => { expect(client.metadata.domain).toEqual(domain); }); }); describe('Requirement 1.3.1, 1.3.2.1', () => { let client: Client; beforeEach(() => { client = OpenFeature.getClient(); }); describe('flag evaluation', () => { describe('getBooleanValue', () => { it('should return boolean, and call boolean resolver', async () => { const booleanFlag = 'my-boolean-flag'; const defaultBooleanValue = false; const value = await client.getBooleanValue(booleanFlag, defaultBooleanValue); expect(value).toEqual(BOOLEAN_VALUE); expect(MOCK_PROVIDER.resolveBooleanEvaluation).toHaveBeenCalledWith(booleanFlag, defaultBooleanValue, {}, {}); }); }); describe('getStringValue', () => { describe('with no generic arg (as string)', () => { it('should return string, and call string resolver', async () => { const stringFlag = 'my-string-flag'; const defaultStringValue = 'default-value'; const value: string = await client.getStringValue(stringFlag, defaultStringValue); expect(value).toEqual(STRING_VALUE); expect(MOCK_PROVIDER.resolveStringEvaluation).toHaveBeenCalledWith(stringFlag, defaultStringValue, {}, {}); }); }); describe('with generic arg', () => { it('should return T, and call string resolver', async () => { const stringFlag = 'my-string-flag'; type MyRestrictedString = 'val' | 'other'; const defaultStringValue = 'other'; const value: MyRestrictedString = await client.getStringValue( stringFlag, defaultStringValue, ); expect(value).toEqual(STRING_VALUE); expect(MOCK_PROVIDER.resolveStringEvaluation).toHaveBeenCalledWith(stringFlag, defaultStringValue, {}, {}); }); }); }); describe('getNumberValue', () => { describe('with no generic arg (as number)', () => { it('should return number, and call number resolver', async () => { const numberFlag = 'my-number-flag'; const defaultNumberValue = 1970; const value: number = await client.getNumberValue(numberFlag, defaultNumberValue); expect(value).toEqual(NUMBER_VALUE); expect(MOCK_PROVIDER.resolveNumberEvaluation).toHaveBeenCalledWith(numberFlag, defaultNumberValue, {}, {}); }); }); describe('with generic arg', () => { it('should return T, and call number resolver', async () => { const numberFlag = 'my-number-flag'; type MyRestrictedNumber = 4096 | 2048; const defaultNumberValue = 4096; const value: MyRestrictedNumber = await client.getNumberValue( numberFlag, defaultNumberValue, ); expect(value).toEqual(NUMBER_VALUE); expect(MOCK_PROVIDER.resolveNumberEvaluation).toHaveBeenCalledWith(numberFlag, defaultNumberValue, {}, {}); }); }); }); describe('getObjectValue', () => { describe('with no generic arg (as JsonValue)', () => { it('should return JsonValue, and call object resolver', async () => { const objectFlag = 'my-object-flag'; const defaultObjectFlag = {}; const value: JsonValue = await client.getObjectValue(objectFlag, defaultObjectFlag); // compare the object expect(value).toEqual(OBJECT_VALUE); // explore the object - type assertions required for safety. const jsonObject: JsonObject = (value as JsonObject)[INNER_KEY] as JsonObject; const nullValue = jsonObject?.[INNER_NULL_KEY] as null; const booleanValue = jsonObject?.[INNER_BOOLEAN_KEY] as boolean; const stringValue = jsonObject?.[INNER_STRING_KEY] as string; const numberValue = jsonObject?.[INNER_NUMBER_KEY] as number; const arrayValue = jsonObject?.[INNER_ARRAY_KEY] as JsonArray; expect(nullValue).toEqual(null); expect(booleanValue).toEqual(BOOLEAN_VALUE); expect(stringValue).toEqual(STRING_VALUE); expect(numberValue).toEqual(NUMBER_VALUE); expect(arrayValue).toEqual(ARRAY_VALUE); }); }); describe('with generic arg', () => { it('should return T, and call object resolver', async () => { const objectFlag = 'my-object-flag'; type MyType = { inner: { booleanKey: boolean; }; }; const defaultMyTypeFlag: MyType = { inner: { booleanKey: false, }, }; const value: MyType = await client.getObjectValue(objectFlag, defaultMyTypeFlag); const innerBooleanValue: boolean = value.inner.booleanKey; expect(innerBooleanValue).toBeTruthy(); }); }); }); }); }); describe('Requirement 1.4.1', () => { let client: Client; beforeEach(() => { client = OpenFeature.getClient(); }); describe('detailed flag evaluation', () => { describe('getBooleanDetails', () => { it('should return boolean details, and call boolean resolver', async () => { const booleanFlag = 'my-boolean-flag'; const defaultBooleanValue = false; const booleanDetails = await client.getBooleanDetails(booleanFlag, defaultBooleanValue); expect(booleanDetails.value).toEqual(BOOLEAN_VALUE); expect(booleanDetails.variant).toEqual(BOOLEAN_VARIANT); expect(MOCK_PROVIDER.resolveBooleanEvaluation).toHaveBeenCalledWith(booleanFlag, defaultBooleanValue, {}, {}); }); }); describe('getStringDetails', () => { it('should return string details, and call string resolver', async () => { const stringFlag = 'my-string-flag'; const defaultStringValue = 'default-value'; const stringDetails = await client.getStringDetails(stringFlag, defaultStringValue); expect(stringDetails.value).toEqual(STRING_VALUE); expect(stringDetails.variant).toEqual(STRING_VARIANT); expect(MOCK_PROVIDER.resolveStringEvaluation).toHaveBeenCalledWith(stringFlag, defaultStringValue, {}, {}); }); }); describe('getNumberDetails', () => { it('should return number details, and call number resolver', async () => { const numberFlag = 'my-number-flag'; const defaultNumberValue = 1970; const numberDetails = await client.getNumberDetails(numberFlag, defaultNumberValue); expect(numberDetails.value).toEqual(NUMBER_VALUE); expect(numberDetails.variant).toEqual(NUMBER_VARIANT); expect(MOCK_PROVIDER.resolveNumberEvaluation).toHaveBeenCalledWith(numberFlag, defaultNumberValue, {}, {}); }); }); describe('getObjectDetails', () => { it('should return object details, and call object resolver', async () => { const objectFlag = 'my-object-flag'; const defaultObjectFlag = {}; const objectDetails = await client.getObjectDetails(objectFlag, defaultObjectFlag); expect(objectDetails.value).toEqual(OBJECT_VALUE); expect(objectDetails.variant).toEqual(OBJECT_VARIANT); expect(MOCK_PROVIDER.resolveObjectEvaluation).toHaveBeenCalledWith(objectFlag, defaultObjectFlag, {}, {}); }); }); }); }); describe('Requirement 1.4.3.1', () => { describe('generic support', () => { it('should support generics', async () => { // No generic information exists at runtime, but this test has some value in ensuring the generic args still exist in the typings. const client = OpenFeature.getClient(); const details: ResolutionDetails = await client.getObjectDetails('flag', { key: 'value' }); expect(details).toBeDefined(); }); }); }); describe('Evaluation details structure', () => { const flagKey = 'number-details'; const defaultValue = 1970; let details: EvaluationDetails; describe('Normal execution', () => { beforeEach(async () => { const client = OpenFeature.getClient(); details = await client.getNumberDetails(flagKey, defaultValue); expect(details).toBeDefined(); }); describe('Requirement 1.4.2, 1.4.3', () => { it('should contain flag value', () => { expect(details.value).toEqual(NUMBER_VALUE); }); }); describe('Requirement 1.4.4', () => { it('should contain flag key', () => { expect(details.flagKey).toEqual(flagKey); }); }); describe('Requirement 1.4.5', () => { it('should contain flag variant', () => { expect(details.variant).toEqual(NUMBER_VARIANT); }); }); describe('Requirement 1.4.6', () => { it('should contain reason', () => { expect(details.reason).toEqual(REASON); }); }); }); describe('Abnormal execution', () => { const NON_OPEN_FEATURE_ERROR_MESSAGE = 'A null dereference or something, I dunno.'; const OPEN_FEATURE_ERROR_MESSAGE = "This ain't the flag you're looking for."; let nonOpenFeatureErrorDetails: EvaluationDetails; let openFeatureErrorDetails: EvaluationDetails; let client: Client; const errorProvider = { metadata: { name: 'error-mock', }, resolveNumberEvaluation: jest.fn((): Promise> => { throw new Error(NON_OPEN_FEATURE_ERROR_MESSAGE); // throw a non-open-feature error }), resolveStringEvaluation: jest.fn((): Promise> => { throw new FlagNotFoundError(OPEN_FEATURE_ERROR_MESSAGE); // throw an open-feature error }), } as unknown as Provider; const defaultNumberValue = 123; const defaultStringValue = 'hey!'; beforeEach(async () => { OpenFeature.setProvider(errorProvider); client = OpenFeature.getClient(); nonOpenFeatureErrorDetails = await client.getNumberDetails('some-flag', defaultNumberValue); openFeatureErrorDetails = await client.getStringDetails('some-flag', defaultStringValue); }); describe('Requirement 1.4.7', () => { describe('OpenFeatureError', () => { it('should contain error code', () => { expect(openFeatureErrorDetails.errorCode).toBeTruthy(); expect(openFeatureErrorDetails.errorCode).toEqual(ErrorCode.FLAG_NOT_FOUND); // should get code from thrown OpenFeatureError }); }); describe('Non-OpenFeatureError', () => { it('should contain error code', () => { expect(nonOpenFeatureErrorDetails.errorCode).toBeTruthy(); expect(nonOpenFeatureErrorDetails.errorCode).toEqual(ErrorCode.GENERAL); // should fall back to GENERAL }); }); }); describe('Requirement 1.4.8', () => { it('should contain error reason', () => { expect(nonOpenFeatureErrorDetails.reason).toEqual(StandardResolutionReasons.ERROR); expect(openFeatureErrorDetails.reason).toEqual(StandardResolutionReasons.ERROR); }); }); describe('Requirement 1.4.9', () => { it('must not throw, must return default', async () => { nonOpenFeatureErrorDetails = await client.getNumberDetails('some-flag', defaultNumberValue); expect(nonOpenFeatureErrorDetails).toBeTruthy(); expect(nonOpenFeatureErrorDetails.value).toEqual(defaultNumberValue); }); }); describe('Requirement 1.4.12', () => { describe('OpenFeatureError', () => { it('should contain "error" message', () => { expect(openFeatureErrorDetails.errorMessage).toEqual(OPEN_FEATURE_ERROR_MESSAGE); }); }); describe('Non-OpenFeatureError', () => { it('should contain "error" message', () => { expect(nonOpenFeatureErrorDetails.errorMessage).toEqual(NON_OPEN_FEATURE_ERROR_MESSAGE); }); }); }); }); }); describe('Requirement 1.4.13, Requirement 1.4.14', () => { it('should return immutable `flag metadata` as defined by the provider', async () => { const flagMetadata = { url: 'https://test.dev', version: '1', }; const flagMetadataProvider = { metadata: { name: 'flag-metadata', }, resolveBooleanEvaluation: jest.fn((): Promise> => { return Promise.resolve({ value: true, flagMetadata, }); }), } as unknown as Provider; OpenFeature.setProvider(flagMetadataProvider); const client = OpenFeature.getClient(); const response = await client.getBooleanDetails('some-flag', false); expect(response.flagMetadata).toBe(flagMetadata); expect(Object.isFrozen(response.flagMetadata)).toBeTruthy(); }); it('should return empty `flag metadata` because it was not set by the provider', async () => { // The mock provider doesn't contain flag metadata OpenFeature.setProvider(MOCK_PROVIDER); const client = OpenFeature.getClient(); const response = await client.getBooleanDetails('some-flag', false); expect(response.flagMetadata).toEqual({}); }); }); describe('Requirement 1.6.1', () => { describe('Provider', () => { const nonTransformingProvider = { metadata: { name: 'non-transforming', }, resolveBooleanEvaluation: jest.fn((): Promise> => { return Promise.resolve({ value: true, }); }), } as unknown as Provider; it('should pass context to resolver', async () => { const flagKey = 'some-other-flag'; const defaultValue = false; const context = { transformed: false }; OpenFeature.setProvider(nonTransformingProvider); const client = OpenFeature.getClient(); await client.getBooleanValue(flagKey, defaultValue, context); // expect context was passed to resolver. expect(nonTransformingProvider.resolveBooleanEvaluation).toHaveBeenCalledWith( flagKey, defaultValue, expect.objectContaining({ transformed: false }), {}, ); }); }); }); describe('Requirement 1.7.1, 1.7.3', () => { const initProvider = { metadata: { name: 'initProvider', }, initialize: () => { return Promise.resolve(); }, } as unknown as Provider; it('status must be READY if init resolves', async () => { await OpenFeature.setProviderAndWait('1.7.1, 1.7.3', initProvider); const client = OpenFeature.getClient('1.7.1, 1.7.3'); expect(client.providerStatus).toEqual(ProviderStatus.READY); }); }); describe('Requirement 1.7.4', () => { const errorProvider = { metadata: { name: 'errorProvider', }, initialize: async () => { return Promise.reject(new GeneralError()); }, } as unknown as Provider; it('status must be ERROR if init rejects', async () => { await expect(OpenFeature.setProviderAndWait('1.7.4', errorProvider)).rejects.toThrow(); const client = OpenFeature.getClient('1.7.4'); expect(client.providerStatus).toEqual(ProviderStatus.ERROR); }); }); describe('Requirement 1.7.5, 1.7.6, 1.7.8', () => { const fatalProvider = { metadata: { name: 'fatalProvider', }, initialize: () => { return Promise.reject(new ProviderFatalError()); }, } as unknown as Provider; it('must short circuit and return PROVIDER_FATAL code if provider FATAL', async () => { await expect(OpenFeature.setProviderAndWait('1.7.5, 1.7.6, 1.7.8', fatalProvider)).rejects.toThrow(); const client = OpenFeature.getClient('1.7.5, 1.7.6, 1.7.8'); expect(client.providerStatus).toEqual(ProviderStatus.FATAL); const defaultVal = 'default'; const details = await client.getStringDetails('some-flag', defaultVal); expect(details.value).toEqual(defaultVal); expect(details.errorCode).toEqual(ErrorCode.PROVIDER_FATAL); }); }); describe('Requirement 1.7.7', () => { const neverReadyProvider = { metadata: { name: 'fatalProvider', }, initialize: () => { return new Promise(() => { return; // promise never resolves }); }, } as unknown as Provider; it('must short circuit and return PROVIDER_NOT_READY code if provider NOT_READY', async () => { OpenFeature.setProviderAndWait('1.7.7', neverReadyProvider).catch(() => { // do nothing }); const defaultVal = 'default'; const client = OpenFeature.getClient('1.7.7'); expect(client.providerStatus).toEqual(ProviderStatus.NOT_READY); const details = await client.getStringDetails('some-flag', defaultVal); expect(details.value).toEqual(defaultVal); expect(details.errorCode).toEqual(ErrorCode.PROVIDER_NOT_READY); }); }); describe('Evaluation Context', () => { const provider = { metadata: { name: 'evaluation-context', }, resolveBooleanEvaluation: jest.fn((): Promise> => { return Promise.resolve({ value: true, }); }), } as unknown as Provider; describe('3.1.1', () => { const TARGETING_KEY = 'abc123'; it('context define targeting key', async () => { const flagKey = 'some-other-flag'; const defaultValue = false; const context: EvaluationContext = { targetingKey: TARGETING_KEY, }; OpenFeature.setProvider(provider); const client = OpenFeature.getClient(); await client.getBooleanValue(flagKey, defaultValue, context); expect(provider.resolveBooleanEvaluation).toHaveBeenCalledWith( expect.anything(), expect.anything(), expect.objectContaining({ targetingKey: TARGETING_KEY, }), expect.anything(), ); }); }); describe('3.1.2', () => { it('should support boolean | string | number | datetime | structure', async () => { const flagKey = 'some-other-flag'; const defaultValue = false; const context: EvaluationContext = { booleanField: BOOLEAN_VALUE, stringField: STRING_VALUE, numberField: NUMBER_VALUE, datetimeField: DATETIME_VALUE, structureField: OBJECT_VALUE, }; OpenFeature.setProvider(provider); const client = OpenFeature.getClient(); await client.getBooleanValue(flagKey, defaultValue, context); expect(provider.resolveBooleanEvaluation).toHaveBeenCalledWith( expect.anything(), expect.anything(), expect.objectContaining({ ...context, }), expect.anything(), ); }); }); describe('3.2.1, 3.2.2', () => { it('Evaluation context MUST be merged in the order: API (global; lowest precedence) -> transaction context -> client -> invocation -> before hooks (highest precedence), with duplicate values being overwritten.', async () => { const flagKey = 'some-other-flag'; const defaultValue = false; const globalContext: EvaluationContext = { globalContextValue: 'abc', globalContextValueToOverwrite: 'xxx', // should be overwritten }; const transactionContext: TransactionContext = { transactionContextValue: 'def', transactionContextValueToOverwrite: 'xxx', // should be overwritten globalContextValueToOverwrite: '123', }; const clientContext: EvaluationContext = { clientContextValue: 'ghi', clientContextValueToOverwrite: 'xxx', // should be overwritten transactionContextValueToOverwrite: '456', }; const invocationContext: EvaluationContext = { invocationContextValue: 'jkl', invocationContextValueToOverwrite: 'xxx', // should be overwritten clientContextValueToOverwrite: '789', }; const beforeHookContext: EvaluationContext = { invocationContextValueToOverwrite: '012', beforeHookContextValue: 'mno', }; // Set Global Context OpenFeature.setProvider(provider).setContext(globalContext); // Set Transaction Context class LocalTransactionContextPropagator implements TransactionContextPropagator { private context: TransactionContext = {}; getTransactionContext(): EvaluationContext { return this.context; } setTransactionContext( transactionContext: TransactionContext, callback: (...args: TArgs) => R, ...args: TArgs ): void { this.context = transactionContext; callback(...args); } } OpenFeature.setTransactionContextPropagator(new LocalTransactionContextPropagator()); OpenFeature.setTransactionContext(transactionContext, jest.fn()); // Set Client Context const client = OpenFeature.getClient('contextual', 'test', clientContext); // Set Hook Context const hook = { before: jest.fn((hookContext: HookContext) => { // we have to put this assertion here because of limitations in jest with expect.objectContaining and mutability if ( isDeepStrictEqual(hookContext.context, { ...globalContext, ...transactionContext, ...clientContext, ...invocationContext, // before hook context should be missing here (and not overridden) }) ) { return beforeHookContext; } }), after: jest.fn((hookContext: HookContext) => { // we have to put this assertion here because of limitations in jest with expect.objectContaining and mutability if ( isDeepStrictEqual(hookContext.context, { ...globalContext, ...transactionContext, ...clientContext, ...invocationContext, ...beforeHookContext, }) ) { return beforeHookContext; } }), } as unknown as Hook; await client.getBooleanValue(flagKey, defaultValue, invocationContext, { hooks: [hook] }); expect(provider.resolveBooleanEvaluation).toHaveBeenCalledWith( expect.anything(), expect.anything(), // expect merged in the correct order... expect.objectContaining({ ...globalContext, ...transactionContext, ...clientContext, ...invocationContext, ...beforeHookContext, }), expect.anything(), ); }); }); describe('client evaluation context', () => { it('can be mutated', async () => { const KEY = 'key'; const VAL = 'val'; const client = OpenFeature.getClient(); client.setContext({ [KEY]: VAL }); expect(client.getContext()[KEY]).toEqual(VAL); }); }); }); it('should be chainable', async () => { const client = OpenFeature.getClient(); expect(await client.addHooks().clearHooks().setContext({}).setLogger(console).getBooleanValue('test', true)).toBe( true, ); }); describe('tracking', () => { describe('Requirement 2.7.1, Requirement 6.1.2.1', () => { const eventName = 'test-tracking-event'; const trackingValue = 1234; const trackingDetails: TrackingEventDetails = { value: trackingValue, }; const globalContextKey = 'globalKey'; const clientContextKey = 'clientKey'; const invocationContextKey = 'invocationKey'; const globalContextValue = 'globalValue'; const clientContextValue = 'clientValue'; const invocationContextValue = 'invocationValue'; it('should no-op and not throw if tracking not defined on provider', async () => { await OpenFeature.setProviderAndWait({ ...MOCK_PROVIDER, track: undefined }); const client = OpenFeature.getClient(); expect(() => { client.track(eventName, {}, trackingDetails); }).not.toThrow(); }); it('should no-op and not throw if provider throws', async () => { await OpenFeature.setProviderAndWait({ ...MOCK_PROVIDER, track: () => { throw new Error('fake error'); }, }); const client = OpenFeature.getClient(); expect(() => { client.track(eventName, {}, trackingDetails); }).not.toThrow(); }); it('provide empty tracking details to provider if not supplied in call', async () => { await OpenFeature.setProviderAndWait({ ...MOCK_PROVIDER }); const client = OpenFeature.getClient(); client.track(eventName); expect(MOCK_PROVIDER.track).toHaveBeenCalledWith( eventName, expect.any(Object), expect.any(Object), ); }); it('should call provider with correct context', async () => { await OpenFeature.setProviderAndWait({ ...MOCK_PROVIDER }); OpenFeature.setContext({ [globalContextKey]: globalContextValue }); const client = OpenFeature.getClient(); client.setContext({ [clientContextKey]: clientContextValue }); client.track(eventName, { [invocationContextKey]: invocationContextValue }, trackingDetails); expect(MOCK_PROVIDER.track).toHaveBeenCalledWith( eventName, expect.objectContaining({ [globalContextKey]: globalContextValue, [clientContextKey]: clientContextValue, [invocationContextKey]: invocationContextValue, }), expect.objectContaining({ value: trackingValue }), ); }); }); }); });