@terreno/rtk 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/emptyApi.d.ts +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/openFeatureReactSdkSpike.test.d.ts +2 -0
- package/dist/openFeatureReactSdkSpike.test.d.ts.map +1 -0
- package/dist/openFeatureReactSdkSpike.test.js +29 -0
- package/dist/openFeatureReactSdkSpike.test.js.map +1 -0
- package/dist/useFeatureFlags.d.ts +15 -15
- package/dist/useFeatureFlags.d.ts.map +1 -1
- package/dist/useFeatureFlags.js +65 -78
- package/dist/useFeatureFlags.js.map +1 -1
- package/dist/useFeatureFlags.test.js +119 -53
- package/dist/useFeatureFlags.test.js.map +1 -1
- package/dist/useTerrenoFeatureFlags.d.ts +39 -0
- package/dist/useTerrenoFeatureFlags.d.ts.map +1 -0
- package/dist/useTerrenoFeatureFlags.js +157 -0
- package/dist/useTerrenoFeatureFlags.js.map +1 -0
- package/dist/useTerrenoFeatureFlags.test.d.ts +2 -0
- package/dist/useTerrenoFeatureFlags.test.d.ts.map +1 -0
- package/dist/useTerrenoFeatureFlags.test.js +160 -0
- package/dist/useTerrenoFeatureFlags.test.js.map +1 -0
- package/package.json +7 -2
- package/src/index.ts +1 -0
- package/src/openFeatureReactSdkSpike.test.ts +36 -0
- package/src/useFeatureFlags.test.ts +143 -57
- package/src/useFeatureFlags.ts +91 -95
- package/src/useTerrenoFeatureFlags.test.ts +222 -0
- package/src/useTerrenoFeatureFlags.ts +226 -0
package/src/useFeatureFlags.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {Api} from "@reduxjs/toolkit/query/react";
|
|
2
|
-
import {useCallback, useEffect, useRef} from "react";
|
|
2
|
+
import {useCallback, useEffect, useMemo, useRef} from "react";
|
|
3
|
+
import {useTerrenoFeatureFlags} from "./useTerrenoFeatureFlags";
|
|
3
4
|
|
|
4
5
|
interface FlagValues {
|
|
5
6
|
[key: string]: boolean | string | null;
|
|
@@ -8,56 +9,19 @@ interface FlagValues {
|
|
|
8
9
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query API generic typing is intentionally flexible here.
|
|
9
10
|
type FlagsApi = Api<any, any, any, any>;
|
|
10
11
|
|
|
11
|
-
// biome-ignore lint/suspicious/noExplicitAny: Endpoint hook is injected dynamically by RTK Query.
|
|
12
|
-
type EnhancedFlagsApi = FlagsApi & {useEvaluateFeatureFlagsQuery: any};
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Cache the enhanced api per (api, basePath). `injectEndpoints` logs a console
|
|
16
|
-
* error in development whenever an endpoint with the same name is re-injected
|
|
17
|
-
* with `overrideExisting: false`, so calling it on every render of every
|
|
18
|
-
* consumer would flood the console. WeakMap-by-api lets the GC reclaim entries
|
|
19
|
-
* when the api object is unreachable.
|
|
20
|
-
*/
|
|
21
|
-
const enhancedApiCache = new WeakMap<FlagsApi, Map<string, EnhancedFlagsApi>>();
|
|
22
|
-
|
|
23
|
-
const getEnhancedApi = (api: FlagsApi, basePath: string): EnhancedFlagsApi => {
|
|
24
|
-
let byBase = enhancedApiCache.get(api);
|
|
25
|
-
if (!byBase) {
|
|
26
|
-
byBase = new Map();
|
|
27
|
-
enhancedApiCache.set(api, byBase);
|
|
28
|
-
}
|
|
29
|
-
const cached = byBase.get(basePath);
|
|
30
|
-
if (cached) {
|
|
31
|
-
return cached;
|
|
32
|
-
}
|
|
33
|
-
const enhanced = api.injectEndpoints({
|
|
34
|
-
endpoints: (builder) => ({
|
|
35
|
-
evaluateFeatureFlags: builder.query<FlagValues, void>({
|
|
36
|
-
query: () => ({
|
|
37
|
-
method: "GET",
|
|
38
|
-
url: `${basePath}/evaluate`,
|
|
39
|
-
}),
|
|
40
|
-
}),
|
|
41
|
-
}),
|
|
42
|
-
overrideExisting: false,
|
|
43
|
-
}) as EnhancedFlagsApi;
|
|
44
|
-
byBase.set(basePath, enhanced);
|
|
45
|
-
return enhanced;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
12
|
interface UseFeatureFlagsResult {
|
|
13
|
+
error: unknown;
|
|
49
14
|
flags: FlagValues;
|
|
50
15
|
getFlag: (key: string) => boolean;
|
|
51
16
|
getVariant: (key: string) => string | null;
|
|
52
17
|
isLoading: boolean;
|
|
53
|
-
error: unknown;
|
|
54
18
|
refetch: () => void;
|
|
55
19
|
}
|
|
56
20
|
|
|
57
21
|
/**
|
|
58
22
|
* Creates feature flag accessors from an RTK Query API instance.
|
|
59
23
|
*
|
|
60
|
-
* Injects a `GET {basePath}/
|
|
24
|
+
* Injects a `GET {basePath}/flagConfiguration` endpoint into the API and returns
|
|
61
25
|
* accessors for reading flag values. Fetches once on mount and caches via
|
|
62
26
|
* RTK Query. Both `api` and `basePath` should be stable references.
|
|
63
27
|
*
|
|
@@ -70,116 +34,148 @@ interface UseFeatureFlagsResult {
|
|
|
70
34
|
* ```
|
|
71
35
|
*/
|
|
72
36
|
export interface UseFeatureFlagsOptions {
|
|
73
|
-
/**
|
|
74
|
-
* Base path for the feature-flags endpoint. Defaults to "/feature-flags".
|
|
75
|
-
*/
|
|
76
37
|
basePath?: string;
|
|
77
|
-
|
|
78
|
-
* When true, the underlying evaluate query is not fired. Use this to avoid
|
|
79
|
-
* fetching before the user is authenticated.
|
|
80
|
-
*/
|
|
38
|
+
domain?: string;
|
|
81
39
|
skip?: boolean;
|
|
40
|
+
socket?: {
|
|
41
|
+
off: (event: string, handler: (...args: unknown[]) => void) => void;
|
|
42
|
+
on: (event: string, handler: (...args: unknown[]) => void) => void;
|
|
43
|
+
} | null;
|
|
44
|
+
socketEventName?: string;
|
|
45
|
+
userId?: string | null;
|
|
82
46
|
}
|
|
83
47
|
|
|
84
48
|
interface ResolvedFeatureFlagsOptions {
|
|
85
49
|
basePath: string;
|
|
50
|
+
domain: string;
|
|
86
51
|
skip: boolean;
|
|
52
|
+
socket?: UseFeatureFlagsOptions["socket"];
|
|
53
|
+
socketEventName?: string;
|
|
54
|
+
userId?: string | null;
|
|
87
55
|
}
|
|
88
56
|
|
|
89
57
|
/**
|
|
90
58
|
* Normalizes the legacy-compatible `basePathOrOptions` argument into a
|
|
91
|
-
* `{basePath, skip}` pair with defaults applied.
|
|
92
|
-
*
|
|
93
|
-
* - `undefined` -> `{basePath: "/feature-flags", skip: false}`
|
|
94
|
-
* - `string` -> `{basePath: <string>, skip: false}` (legacy form)
|
|
95
|
-
* - `object` -> `{basePath: opts.basePath ?? "/feature-flags", skip: opts.skip ?? false}`
|
|
59
|
+
* `{basePath, skip, ...}` pair with defaults applied.
|
|
96
60
|
*/
|
|
97
61
|
export const resolveFeatureFlagsOptions = (
|
|
98
62
|
basePathOrOptions?: string | UseFeatureFlagsOptions
|
|
99
63
|
): ResolvedFeatureFlagsOptions => {
|
|
100
|
-
const
|
|
64
|
+
const raw =
|
|
101
65
|
typeof basePathOrOptions === "string"
|
|
102
66
|
? {basePath: basePathOrOptions, skip: false}
|
|
103
67
|
: (basePathOrOptions ?? {});
|
|
104
|
-
return {
|
|
68
|
+
return {
|
|
69
|
+
basePath: raw.basePath ?? "/feature-flags",
|
|
70
|
+
domain: raw.domain ?? "feature-flags",
|
|
71
|
+
skip: raw.skip ?? false,
|
|
72
|
+
socket: raw.socket,
|
|
73
|
+
socketEventName: raw.socketEventName,
|
|
74
|
+
userId: raw.userId,
|
|
75
|
+
};
|
|
105
76
|
};
|
|
106
77
|
|
|
107
|
-
|
|
108
|
-
// pass a string basePath as the second argument.
|
|
109
|
-
export function useFeatureFlags(
|
|
78
|
+
export const useFeatureFlags = (
|
|
110
79
|
api: FlagsApi,
|
|
111
80
|
basePathOrOptions?: string | UseFeatureFlagsOptions
|
|
112
|
-
): UseFeatureFlagsResult {
|
|
113
|
-
const {basePath, skip} =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
81
|
+
): UseFeatureFlagsResult => {
|
|
82
|
+
const {basePath, domain, skip, socket, socketEventName, userId} =
|
|
83
|
+
resolveFeatureFlagsOptions(basePathOrOptions);
|
|
84
|
+
|
|
85
|
+
const {
|
|
86
|
+
client,
|
|
87
|
+
error,
|
|
88
|
+
flags: rawFlags,
|
|
89
|
+
isLoading,
|
|
90
|
+
refetch,
|
|
91
|
+
} = useTerrenoFeatureFlags(api, {
|
|
92
|
+
basePath,
|
|
93
|
+
domain,
|
|
94
|
+
skip,
|
|
95
|
+
socket,
|
|
96
|
+
socketEventName,
|
|
97
|
+
userId,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const fetchStartedAtRef = useRef<number | null>(null);
|
|
119
101
|
|
|
120
|
-
const flags: FlagValues = data ?? {};
|
|
121
|
-
|
|
122
|
-
// Log when evaluate request enters loading state so client timing can be measured.
|
|
123
102
|
useEffect((): void => {
|
|
124
|
-
if (!isLoading ||
|
|
103
|
+
if (!isLoading || fetchStartedAtRef.current !== null) {
|
|
125
104
|
return;
|
|
126
105
|
}
|
|
127
106
|
|
|
128
|
-
|
|
129
|
-
console.debug("[feature-flags]
|
|
107
|
+
fetchStartedAtRef.current = Date.now();
|
|
108
|
+
console.debug("[feature-flags] flagConfiguration request started", {
|
|
130
109
|
basePath,
|
|
131
110
|
});
|
|
132
111
|
}, [basePath, isLoading]);
|
|
133
112
|
|
|
134
|
-
// Log evaluate success with duration and number of resolved flag values.
|
|
135
113
|
useEffect((): void => {
|
|
136
|
-
if (!
|
|
114
|
+
if (!error || fetchStartedAtRef.current === null) {
|
|
137
115
|
return;
|
|
138
116
|
}
|
|
139
117
|
|
|
140
|
-
const durationMs = Date.now() -
|
|
141
|
-
|
|
142
|
-
console.debug("[feature-flags]
|
|
118
|
+
const durationMs = Date.now() - fetchStartedAtRef.current;
|
|
119
|
+
fetchStartedAtRef.current = null;
|
|
120
|
+
console.debug("[feature-flags] flagConfiguration request failed", {
|
|
143
121
|
basePath,
|
|
144
122
|
durationMs,
|
|
145
|
-
|
|
146
|
-
evaluatedFlags: flags,
|
|
123
|
+
error,
|
|
147
124
|
});
|
|
148
|
-
}, [basePath,
|
|
125
|
+
}, [basePath, error]);
|
|
126
|
+
|
|
127
|
+
const flatFlags = useMemo((): FlagValues => {
|
|
128
|
+
const out: FlagValues = {};
|
|
129
|
+
for (const [key, def] of Object.entries(rawFlags)) {
|
|
130
|
+
const value = def.variants[def.defaultVariant];
|
|
131
|
+
out[key] = value ?? null;
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}, [rawFlags]);
|
|
149
135
|
|
|
150
|
-
// Log evaluate failures with duration so request issues can be correlated.
|
|
151
136
|
useEffect((): void => {
|
|
152
|
-
if (
|
|
137
|
+
if (Object.keys(flatFlags).length === 0 || fetchStartedAtRef.current === null) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (isLoading) {
|
|
153
142
|
return;
|
|
154
143
|
}
|
|
155
144
|
|
|
156
|
-
const durationMs = Date.now() -
|
|
157
|
-
|
|
158
|
-
console.debug("[feature-flags]
|
|
145
|
+
const durationMs = Date.now() - fetchStartedAtRef.current;
|
|
146
|
+
fetchStartedAtRef.current = null;
|
|
147
|
+
console.debug("[feature-flags] flagConfiguration request completed", {
|
|
159
148
|
basePath,
|
|
160
149
|
durationMs,
|
|
161
|
-
|
|
150
|
+
evaluatedFlagCount: Object.keys(flatFlags).length,
|
|
151
|
+
evaluatedFlags: flatFlags,
|
|
162
152
|
});
|
|
163
|
-
}, [basePath,
|
|
153
|
+
}, [basePath, flatFlags, isLoading]);
|
|
164
154
|
|
|
165
155
|
const getFlag = useCallback(
|
|
166
156
|
(key: string): boolean => {
|
|
167
|
-
|
|
168
|
-
return value === true;
|
|
157
|
+
return client.getBooleanValue(key, false);
|
|
169
158
|
},
|
|
170
|
-
[
|
|
159
|
+
[client]
|
|
171
160
|
);
|
|
172
161
|
|
|
173
162
|
const getVariant = useCallback(
|
|
174
163
|
(key: string): string | null => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
164
|
+
if (!rawFlags[key]) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const value = client.getStringValue(key, "");
|
|
168
|
+
if (value === "") {
|
|
169
|
+
return null;
|
|
178
170
|
}
|
|
179
|
-
return
|
|
171
|
+
return value;
|
|
180
172
|
},
|
|
181
|
-
[
|
|
173
|
+
[client, rawFlags]
|
|
182
174
|
);
|
|
183
175
|
|
|
184
|
-
|
|
185
|
-
|
|
176
|
+
const stableRefetch = useCallback((): void => {
|
|
177
|
+
void refetch();
|
|
178
|
+
}, [refetch]);
|
|
179
|
+
|
|
180
|
+
return {error, flags: flatFlags, getFlag, getVariant, isLoading, refetch: stableRefetch};
|
|
181
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
|
|
2
|
+
import EventEmitter from "node:events";
|
|
3
|
+
import {NOOP_PROVIDER, OpenFeature} from "@openfeature/web-sdk";
|
|
4
|
+
import {renderHook, waitFor} from "@testing-library/react-native";
|
|
5
|
+
import React, {StrictMode} from "react";
|
|
6
|
+
|
|
7
|
+
import {useTerrenoFeatureFlags} from "./useTerrenoFeatureFlags";
|
|
8
|
+
|
|
9
|
+
type TerrenoFlagConfiguration = Record<
|
|
10
|
+
string,
|
|
11
|
+
{
|
|
12
|
+
defaultVariant: string;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
variants: Record<string, boolean | string>;
|
|
15
|
+
}
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
type QueryResult = {
|
|
19
|
+
data?: TerrenoFlagConfiguration;
|
|
20
|
+
error?: unknown;
|
|
21
|
+
isError?: boolean;
|
|
22
|
+
isFetching?: boolean;
|
|
23
|
+
isLoading?: boolean;
|
|
24
|
+
isSuccess?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const boolDef = (variant: "on" | "off") => ({
|
|
28
|
+
defaultVariant: variant,
|
|
29
|
+
disabled: false,
|
|
30
|
+
variants: {off: false, on: true},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const buildApi = (queryResult: QueryResult) => {
|
|
34
|
+
const refetch = mock(() => {});
|
|
35
|
+
const useTerrenoFlagConfigurationQuery = mock(() => {
|
|
36
|
+
const hasData = Boolean(queryResult.data);
|
|
37
|
+
return {
|
|
38
|
+
data: queryResult.data,
|
|
39
|
+
error: queryResult.error,
|
|
40
|
+
isError: queryResult.isError ?? Boolean(queryResult.error),
|
|
41
|
+
isFetching: queryResult.isFetching ?? false,
|
|
42
|
+
isLoading: queryResult.isLoading ?? false,
|
|
43
|
+
isSuccess: queryResult.isSuccess ?? hasData,
|
|
44
|
+
refetch,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
const api = {
|
|
48
|
+
injectEndpoints: mock((opts: {endpoints: (builder: unknown) => void}) => {
|
|
49
|
+
const builder = {
|
|
50
|
+
query: (def: {query: () => {method: string; url: string}}) => {
|
|
51
|
+
def.query();
|
|
52
|
+
return {useQuery: useTerrenoFlagConfigurationQuery};
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
opts.endpoints(builder);
|
|
56
|
+
return {useTerrenoFlagConfigurationQuery};
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
return {api, refetch, useTerrenoFlagConfigurationQuery};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe("useTerrenoFeatureFlags", () => {
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
await OpenFeature.setProviderAndWait("feature-flags", NOOP_PROVIDER);
|
|
65
|
+
await OpenFeature.setProviderAndWait("custom-ff", NOOP_PROVIDER);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(async () => {
|
|
69
|
+
await OpenFeature.setProviderAndWait("feature-flags", NOOP_PROVIDER);
|
|
70
|
+
await OpenFeature.setProviderAndWait("custom-ff", NOOP_PROVIDER);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("sets TypedInMemoryProvider and exposes client values after success", async () => {
|
|
74
|
+
const {api} = buildApi({data: {alpha: boolDef("on")}});
|
|
75
|
+
const {result} = renderHook(() =>
|
|
76
|
+
useTerrenoFeatureFlags(api as never, {domain: "feature-flags", userId: "u1"})
|
|
77
|
+
);
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(result.current.isLoading).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
expect(result.current.client.getBooleanValue("alpha", false)).toBe(true);
|
|
82
|
+
expect(result.current.flags.alpha?.defaultVariant).toBe("on");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("does not leave loading stuck when skip is true", () => {
|
|
86
|
+
const {api} = buildApi({data: {alpha: boolDef("on")}});
|
|
87
|
+
const {result} = renderHook(() =>
|
|
88
|
+
useTerrenoFeatureFlags(api as never, {domain: "feature-flags", skip: true, userId: "u1"})
|
|
89
|
+
);
|
|
90
|
+
expect(result.current.isLoading).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("does not leave loading stuck when the flag configuration query errors", () => {
|
|
94
|
+
const {api} = buildApi({
|
|
95
|
+
error: new Error("request failed"),
|
|
96
|
+
isError: true,
|
|
97
|
+
isLoading: false,
|
|
98
|
+
isSuccess: false,
|
|
99
|
+
});
|
|
100
|
+
const {result} = renderHook(() =>
|
|
101
|
+
useTerrenoFeatureFlags(api as never, {domain: "feature-flags", userId: "u1"})
|
|
102
|
+
);
|
|
103
|
+
expect(result.current.isLoading).toBe(false);
|
|
104
|
+
expect(result.current.error).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("isolates domains so a custom domain does not read another domain's provider", async () => {
|
|
108
|
+
const {api} = buildApi({data: {alpha: boolDef("on")}});
|
|
109
|
+
const {result} = renderHook(() =>
|
|
110
|
+
useTerrenoFeatureFlags(api as never, {domain: "custom-ff", userId: "u1"})
|
|
111
|
+
);
|
|
112
|
+
await waitFor(() => {
|
|
113
|
+
expect(result.current.isLoading).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
const defaultClient = OpenFeature.getClient();
|
|
116
|
+
expect(defaultClient.getBooleanValue("alpha", false)).toBe(false);
|
|
117
|
+
expect(result.current.client.getBooleanValue("alpha", false)).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("refetches when the socket receives the configured event", async () => {
|
|
121
|
+
const socket = new EventEmitter();
|
|
122
|
+
const {api, refetch} = buildApi({data: {alpha: boolDef("off")}});
|
|
123
|
+
const {result} = renderHook(() =>
|
|
124
|
+
useTerrenoFeatureFlags(api as never, {
|
|
125
|
+
domain: "feature-flags",
|
|
126
|
+
socket: {
|
|
127
|
+
off: (ev, fn) => {
|
|
128
|
+
socket.off(ev, fn);
|
|
129
|
+
},
|
|
130
|
+
on: (ev, fn) => {
|
|
131
|
+
socket.on(ev, fn);
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
socketEventName: "featureFlagsChanged",
|
|
135
|
+
userId: "u1",
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
await waitFor(() => {
|
|
139
|
+
expect(result.current.isLoading).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
socket.emit("featureFlagsChanged");
|
|
142
|
+
expect(refetch).toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("survives React StrictMode double-mount without throwing", async () => {
|
|
146
|
+
const {api} = buildApi({data: {alpha: boolDef("on")}});
|
|
147
|
+
const {result} = renderHook(
|
|
148
|
+
() => useTerrenoFeatureFlags(api as never, {domain: "feature-flags", userId: "u1"}),
|
|
149
|
+
{
|
|
150
|
+
wrapper: ({children}: {children: React.ReactNode}) =>
|
|
151
|
+
React.createElement(StrictMode, null, children),
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
expect(result.current.isLoading).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
expect(result.current.client.getBooleanValue("alpha", false)).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("re-applies provider when fetched configuration changes", async () => {
|
|
161
|
+
const built = buildApi({data: {alpha: boolDef("off")}});
|
|
162
|
+
const {result, rerender} = renderHook(
|
|
163
|
+
({data}: {data: TerrenoFlagConfiguration}) => {
|
|
164
|
+
built.useTerrenoFlagConfigurationQuery.mockImplementation(() => ({
|
|
165
|
+
data,
|
|
166
|
+
error: undefined,
|
|
167
|
+
isError: false,
|
|
168
|
+
isFetching: false,
|
|
169
|
+
isLoading: false,
|
|
170
|
+
isSuccess: true,
|
|
171
|
+
refetch: built.refetch,
|
|
172
|
+
}));
|
|
173
|
+
return useTerrenoFeatureFlags(built.api as never, {domain: "feature-flags", userId: "u1"});
|
|
174
|
+
},
|
|
175
|
+
{initialProps: {data: {alpha: boolDef("off")}}}
|
|
176
|
+
);
|
|
177
|
+
await waitFor(() => {
|
|
178
|
+
expect(result.current.client.getBooleanValue("alpha", false)).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
rerender({data: {alpha: boolDef("on")}});
|
|
181
|
+
await waitFor(() => {
|
|
182
|
+
expect(result.current.client.getBooleanValue("alpha", false)).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("useTerrenoFeatureFlags ref-count cleanup", () => {
|
|
188
|
+
/** Dedicated domain so parallel test files do not hold extra `feature-flags` hook refs. */
|
|
189
|
+
const refcountTestDomain = "terreno-refcount-test-isolation";
|
|
190
|
+
|
|
191
|
+
beforeEach(async () => {
|
|
192
|
+
await OpenFeature.setProviderAndWait(refcountTestDomain, NOOP_PROVIDER);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
afterEach(async () => {
|
|
196
|
+
await OpenFeature.setProviderAndWait(refcountTestDomain, NOOP_PROVIDER);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("installs NOOP provider after the last hook instance unmounts", async () => {
|
|
200
|
+
const {api} = buildApi({data: {alpha: boolDef("on")}});
|
|
201
|
+
const {unmount} = renderHook(() =>
|
|
202
|
+
useTerrenoFeatureFlags(api as never, {domain: refcountTestDomain, userId: "u1"})
|
|
203
|
+
);
|
|
204
|
+
await waitFor(
|
|
205
|
+
() => {
|
|
206
|
+
expect(OpenFeature.getClient(refcountTestDomain).getBooleanValue("alpha", false)).toBe(
|
|
207
|
+
true
|
|
208
|
+
);
|
|
209
|
+
},
|
|
210
|
+
{timeout: 5000}
|
|
211
|
+
);
|
|
212
|
+
unmount();
|
|
213
|
+
await waitFor(
|
|
214
|
+
() => {
|
|
215
|
+
expect(OpenFeature.getClient(refcountTestDomain).getBooleanValue("alpha", false)).toBe(
|
|
216
|
+
false
|
|
217
|
+
);
|
|
218
|
+
},
|
|
219
|
+
{timeout: 5000}
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
});
|