From 59decd51aae01fc1fd1d7901d31e655527aad162 Mon Sep 17 00:00:00 2001 From: marcozabel Date: Fri, 19 Dec 2025 14:50:10 +0100 Subject: [PATCH 1/4] Add evaluation options to FeatureFlag component Signed-off-by: marcozabel --- .../react/src/declarative/FeatureFlag.tsx | 8 ++ packages/react/test/declarative.spec.tsx | 81 ++++++++++++++++++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/packages/react/src/declarative/FeatureFlag.tsx b/packages/react/src/declarative/FeatureFlag.tsx index d318f3e46..3edde9ba7 100644 --- a/packages/react/src/declarative/FeatureFlag.tsx +++ b/packages/react/src/declarative/FeatureFlag.tsx @@ -3,6 +3,7 @@ import { useFlag } from '../evaluation'; import type { FlagQuery } from '../query'; import type { FlagValue, EvaluationDetails } from '@openfeature/core'; import { isEqual } from '../internal'; +import type { ReactFlagEvaluationOptions } from '../options'; /** * Default predicate function that checks if the expected value equals the actual flag value. @@ -44,6 +45,11 @@ interface FeatureFlagProps { * Can be a React node or a function that receives evaluation details and returns a React node. */ fallback?: React.ReactNode | ((details: EvaluationDetails) => React.ReactNode); + + /** + * Flag evaluation options that will be passed to useFlag hook. + */ + evaluationOptions?: ReactFlagEvaluationOptions; } /** @@ -86,10 +92,12 @@ export function FeatureFlag({ predicate, defaultValue, children, + evaluationOptions = {}, fallback = null, }: FeatureFlagComponentProps): React.ReactElement | null { const details = useFlag(flagKey, defaultValue, { updateOnContextChanged: true, + ...evaluationOptions }); // If the flag evaluation failed, we render the fallback diff --git a/packages/react/test/declarative.spec.tsx b/packages/react/test/declarative.spec.tsx index f5c536a96..16d5798d4 100644 --- a/packages/react/test/declarative.spec.tsx +++ b/packages/react/test/declarative.spec.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { Suspense } from 'react'; import '@testing-library/jest-dom'; // see: https://testing-library.com/docs/react-testing-library/setup import { render, screen } from '@testing-library/react'; import { FeatureFlag } from '../src/declarative/FeatureFlag'; // Assuming Feature.tsx is in the same directory or adjust path -import { InMemoryProvider, OpenFeature, OpenFeatureProvider } from '../src'; -import type { EvaluationDetails } from '@openfeature/core'; +import type { Provider} from '../src'; +import { InMemoryProvider, OpenFeature, OpenFeatureProvider, ProviderStatus } from '../src'; +import type { EvaluationDetails, JsonValue, ResolutionDetails } from '@openfeature/core'; describe('Feature Component', () => { const EVALUATION = 'evaluation'; @@ -47,6 +48,39 @@ describe('Feature Component', () => { return new InMemoryProvider(FLAG_CONFIG); }; + const makeAsyncProvider = () => { + class MockAsyncProvider implements Provider { + metadata = { + name: 'mock-async', + }; + + status = ProviderStatus.NOT_READY; + + constructor() {} + + async initialize(): Promise {} + + async setReady(): Promise { + this.status = ProviderStatus.READY; + } + + resolveBooleanEvaluation(): ResolutionDetails { + throw new Error('Method not implemented.'); + } + resolveStringEvaluation(): ResolutionDetails { + throw new Error('Method not implemented.'); + } + resolveNumberEvaluation(): ResolutionDetails { + throw new Error('Method not implemented.'); + } + resolveObjectEvaluation(): ResolutionDetails { + throw new Error('Method not implemented.'); + } + } + + return new MockAsyncProvider(); + }; + OpenFeature.setProvider(EVALUATION, makeProvider()); const childText = 'Feature is active'; @@ -108,6 +142,47 @@ describe('Feature Component', () => { expect(screen.queryByText(childText)).toBeInTheDocument(); }); + it('should render the fallback if suspended and flag is unresolved', () => { + OpenFeature.setProvider(EVALUATION, makeAsyncProvider()); + + render( + + Suspense}> + + + + + , + ); + + expect(screen.queryByText('Suspense')).toBeInTheDocument(); + + // restore the original provider for other tests + OpenFeature.setProvider(EVALUATION, makeProvider()); + }); + + it('should render child if provider is ready', () => { + render( + + Suspense}> + + + + + , + ); + + expect(screen.queryByText(childText)).toBeInTheDocument(); + }); + it('should support custom predicate function', () => { const customPredicate = (expected: boolean | undefined, actual: { value: boolean }) => { // Custom logic: render if flag is NOT the expected value (negation) From 34bba7a522b85dd43a72232656b3379d66a8a70a Mon Sep 17 00:00:00 2001 From: marcozabel Date: Fri, 19 Dec 2025 14:56:51 +0100 Subject: [PATCH 2/4] Cleanup async mock provider Signed-off-by: marcozabel --- packages/react/test/declarative.spec.tsx | 25 +++--------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/packages/react/test/declarative.spec.tsx b/packages/react/test/declarative.spec.tsx index 16d5798d4..a30fc5333 100644 --- a/packages/react/test/declarative.spec.tsx +++ b/packages/react/test/declarative.spec.tsx @@ -4,7 +4,7 @@ import { render, screen } from '@testing-library/react'; import { FeatureFlag } from '../src/declarative/FeatureFlag'; // Assuming Feature.tsx is in the same directory or adjust path import type { Provider} from '../src'; import { InMemoryProvider, OpenFeature, OpenFeatureProvider, ProviderStatus } from '../src'; -import type { EvaluationDetails, JsonValue, ResolutionDetails } from '@openfeature/core'; +import type { EvaluationDetails } from '@openfeature/core'; describe('Feature Component', () => { const EVALUATION = 'evaluation'; @@ -49,36 +49,17 @@ describe('Feature Component', () => { }; const makeAsyncProvider = () => { - class MockAsyncProvider implements Provider { + class MockAsyncProvider { metadata = { name: 'mock-async', }; - status = ProviderStatus.NOT_READY; constructor() {} - async initialize(): Promise {} - - async setReady(): Promise { - this.status = ProviderStatus.READY; - } - - resolveBooleanEvaluation(): ResolutionDetails { - throw new Error('Method not implemented.'); - } - resolveStringEvaluation(): ResolutionDetails { - throw new Error('Method not implemented.'); - } - resolveNumberEvaluation(): ResolutionDetails { - throw new Error('Method not implemented.'); - } - resolveObjectEvaluation(): ResolutionDetails { - throw new Error('Method not implemented.'); - } } - return new MockAsyncProvider(); + return new MockAsyncProvider() as Provider; }; OpenFeature.setProvider(EVALUATION, makeProvider()); From ccb2c8ca50989671003285108dd2d39f988b13d1 Mon Sep 17 00:00:00 2001 From: marcozabel Date: Fri, 19 Dec 2025 15:01:05 +0100 Subject: [PATCH 3/4] Implement gemini suggestions Signed-off-by: marcozabel --- packages/react/test/declarative.spec.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/react/test/declarative.spec.tsx b/packages/react/test/declarative.spec.tsx index a30fc5333..90451b7a2 100644 --- a/packages/react/test/declarative.spec.tsx +++ b/packages/react/test/declarative.spec.tsx @@ -55,20 +55,18 @@ describe('Feature Component', () => { }; status = ProviderStatus.NOT_READY; - constructor() {} async initialize(): Promise {} } return new MockAsyncProvider() as Provider; }; - OpenFeature.setProvider(EVALUATION, makeProvider()); - const childText = 'Feature is active'; const ChildComponent = () =>
{childText}
; beforeEach(() => { jest.clearAllMocks(); + OpenFeature.setProvider(EVALUATION, makeProvider()); }); describe('', () => { @@ -141,9 +139,6 @@ describe('Feature Component', () => { ); expect(screen.queryByText('Suspense')).toBeInTheDocument(); - - // restore the original provider for other tests - OpenFeature.setProvider(EVALUATION, makeProvider()); }); it('should render child if provider is ready', () => { From 9d47df5bcfbc52d16f3fd116ad370a52b13158f2 Mon Sep 17 00:00:00 2001 From: marcozabel Date: Fri, 19 Dec 2025 15:04:34 +0100 Subject: [PATCH 4/4] Fix formatting Signed-off-by: marcozabel --- .../react/src/declarative/FeatureFlag.tsx | 2 +- packages/react/test/declarative.spec.tsx | 22 ++++++------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/react/src/declarative/FeatureFlag.tsx b/packages/react/src/declarative/FeatureFlag.tsx index 3edde9ba7..fee68fe9f 100644 --- a/packages/react/src/declarative/FeatureFlag.tsx +++ b/packages/react/src/declarative/FeatureFlag.tsx @@ -97,7 +97,7 @@ export function FeatureFlag({ }: FeatureFlagComponentProps): React.ReactElement | null { const details = useFlag(flagKey, defaultValue, { updateOnContextChanged: true, - ...evaluationOptions + ...evaluationOptions, }); // If the flag evaluation failed, we render the fallback diff --git a/packages/react/test/declarative.spec.tsx b/packages/react/test/declarative.spec.tsx index 90451b7a2..182d18792 100644 --- a/packages/react/test/declarative.spec.tsx +++ b/packages/react/test/declarative.spec.tsx @@ -2,7 +2,7 @@ import React, { Suspense } from 'react'; import '@testing-library/jest-dom'; // see: https://testing-library.com/docs/react-testing-library/setup import { render, screen } from '@testing-library/react'; import { FeatureFlag } from '../src/declarative/FeatureFlag'; // Assuming Feature.tsx is in the same directory or adjust path -import type { Provider} from '../src'; +import type { Provider } from '../src'; import { InMemoryProvider, OpenFeature, OpenFeatureProvider, ProviderStatus } from '../src'; import type { EvaluationDetails } from '@openfeature/core'; @@ -127,13 +127,9 @@ describe('Feature Component', () => { render( Suspense}> - - - + + + , ); @@ -145,13 +141,9 @@ describe('Feature Component', () => { render( Suspense}> - - - + + + , );