-
Notifications
You must be signed in to change notification settings - Fork 36
/
Copy pathtest-provider.tsx
122 lines (112 loc) · 4.45 KB
/
test-provider.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import type {
JsonValue,
Provider} from '@openfeature/web-sdk';
import {
InMemoryProvider,
NOOP_PROVIDER,
OpenFeature
} from '@openfeature/web-sdk';
import React from 'react';
import type { NormalizedOptions } from '../options';
import { OpenFeatureProvider } from './provider';
type FlagValueMap = { [flagKey: string]: JsonValue };
type FlagConfig = ConstructorParameters<typeof InMemoryProvider>[0];
type TestProviderProps = Omit<React.ComponentProps<typeof OpenFeatureProvider>, 'client'> &
(
| {
provider?: never;
/**
* Optional map of flagKeys to flagValues for this OpenFeatureTestProvider context.
* If not supplied, all flag evaluations will default.
*/
flagValueMap?: FlagValueMap;
/**
* Optional delay for the underlying test provider's readiness and reconciliation.
* Defaults to 0.
*/
delayMs?: number;
}
| {
/**
* An optional partial provider to pass for full control over the flag resolution for this OpenFeatureTestProvider context.
* Any un-implemented methods or properties will no-op.
*/
provider?: Partial<Provider>;
flagValueMap?: never;
delayMs?: never;
}
);
const TEST_VARIANT = 'test-variant';
const TEST_PROVIDER = 'test-provider';
// internal provider which is basically the in-memory provider with a simpler config and some optional fake delays
class TestProvider extends InMemoryProvider {
// initially make this undefined, we still set it if a delay is specified
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - For maximum compatibility with previous versions, we ignore a possible TS error here,
// since "initialize" was previously defined in superclass.
// We can safely remove this ts-ignore in a few versions
initialize: Provider['initialize'] = undefined;
// "place-holder" init function which we only assign if want a delay
private delayedInitialize = async () => {
await new Promise<void>((resolve) => setTimeout(resolve, this.delay));
};
constructor(
flagValueMap: FlagValueMap,
private delay = 0,
) {
// convert the simple flagValueMap into an in-memory config
const flagConfig = Object.entries(flagValueMap).reduce((acc: FlagConfig, flag): FlagConfig => {
return {
...acc,
[flag[0]]: {
variants: {
[TEST_VARIANT]: flag[1],
},
defaultVariant: TEST_VARIANT,
disabled: false,
},
};
}, {});
super(flagConfig);
// only define and init if there's a non-zero delay specified
this.initialize = this.delay ? this.delayedInitialize.bind(this) : undefined;
}
async onContextChange() {
return new Promise<void>((resolve) => setTimeout(resolve, this.delay));
}
}
/**
* A React Context provider based on the {@link InMemoryProvider}, specifically built for testing.
* Use this for testing components that use flag evaluation hooks.
* @param {TestProviderProps} testProviderOptions options for the OpenFeatureTestProvider
* @returns {OpenFeatureProvider} OpenFeatureTestProvider
*/
export function OpenFeatureTestProvider(testProviderOptions: TestProviderProps) {
const { flagValueMap, provider } = testProviderOptions;
const effectiveProvider = (
flagValueMap ? new TestProvider(flagValueMap, testProviderOptions.delayMs) : mixInNoop(provider) || NOOP_PROVIDER
) as Provider;
testProviderOptions.domain
? OpenFeature.setProvider(testProviderOptions.domain, effectiveProvider)
: OpenFeature.setProvider(effectiveProvider);
return (
<OpenFeatureProvider {...(testProviderOptions as NormalizedOptions)} domain={testProviderOptions.domain}>
{testProviderOptions.children}
</OpenFeatureProvider>
);
}
// mix in the no-op provider when the partial is passed
function mixInNoop(provider: Partial<Provider> = {}) {
// fill in any missing methods with no-ops
for (const prop of Object.getOwnPropertyNames(Object.getPrototypeOf(NOOP_PROVIDER)).filter(prop => prop !== 'constructor')) {
const patchedProvider = provider as {[key: string]: keyof Provider};
if (!Object.getPrototypeOf(patchedProvider)[prop] && !patchedProvider[prop]) {
patchedProvider[prop] = Object.getPrototypeOf(NOOP_PROVIDER)[prop];
}
}
// fill in the metadata if missing
if (!provider.metadata || !provider.metadata.name) {
(provider.metadata as unknown) = { name: TEST_PROVIDER };
}
return provider;
}