Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions packages/react/src/declarative/FeatureFlag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -44,6 +45,11 @@ interface FeatureFlagProps<T extends FlagValue = FlagValue> {
* Can be a React node or a function that receives evaluation details and returns a React node.
*/
fallback?: React.ReactNode | ((details: EvaluationDetails<T>) => React.ReactNode);

/**
* Flag evaluation options that will be passed to useFlag hook.
*/
evaluationOptions?: ReactFlagEvaluationOptions;
}

/**
Expand Down Expand Up @@ -86,10 +92,12 @@ export function FeatureFlag<T extends FlagValue = FlagValue>({
predicate,
defaultValue,
children,
evaluationOptions = {},
fallback = null,
}: FeatureFlagComponentProps<T>): React.ReactElement | null {
const details = useFlag(flagKey, defaultValue, {
updateOnContextChanged: true,
Comment thread
beeme1mr marked this conversation as resolved.
...evaluationOptions
});

// If the flag evaluation failed, we render the fallback
Expand Down
81 changes: 78 additions & 3 deletions packages/react/test/declarative.spec.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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() {}
Comment thread
marcozabel marked this conversation as resolved.
Outdated

async initialize(): Promise<void> {}

async setReady(): Promise<void> {
this.status = ProviderStatus.READY;
}

resolveBooleanEvaluation(): ResolutionDetails<boolean> {
throw new Error('Method not implemented.');
}
resolveStringEvaluation(): ResolutionDetails<string> {
throw new Error('Method not implemented.');
}
resolveNumberEvaluation(): ResolutionDetails<number> {
throw new Error('Method not implemented.');
}
resolveObjectEvaluation<T extends JsonValue>(): ResolutionDetails<T> {
throw new Error('Method not implemented.');
}
}

return new MockAsyncProvider();
};

OpenFeature.setProvider(EVALUATION, makeProvider());

const childText = 'Feature is active';
Expand Down Expand Up @@ -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(
<OpenFeatureProvider domain={EVALUATION}>
<Suspense fallback={<div>Suspense</div>}>
<FeatureFlag
flagKey={BOOL_FLAG_KEY}
defaultValue={false}
evaluationOptions={{ suspend: true }}
>
<ChildComponent />
</FeatureFlag>
</Suspense>
</OpenFeatureProvider>,
);

expect(screen.queryByText('Suspense')).toBeInTheDocument();

// restore the original provider for other tests
OpenFeature.setProvider(EVALUATION, makeProvider());
Comment thread
marcozabel marked this conversation as resolved.
Outdated
});

it('should render child if provider is ready', () => {
render(
<OpenFeatureProvider domain={EVALUATION}>
<Suspense fallback={<div>Suspense</div>}>
<FeatureFlag
flagKey={BOOL_FLAG_KEY}
defaultValue={false}
evaluationOptions={{ suspend: true }}
>
<ChildComponent />
</FeatureFlag>
</Suspense>
</OpenFeatureProvider>,
);

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)
Expand Down
Loading