@terreno/rtk 0.19.0 → 0.20.0
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
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
import { useTerrenoFeatureFlags } from "./useTerrenoFeatureFlags";
|
|
7
|
+
const boolDef = (variant) => ({
|
|
8
|
+
defaultVariant: variant,
|
|
9
|
+
disabled: false,
|
|
10
|
+
variants: { off: false, on: true },
|
|
11
|
+
});
|
|
12
|
+
const buildApi = (queryResult) => {
|
|
13
|
+
const refetch = mock(() => { });
|
|
14
|
+
const useTerrenoFlagConfigurationQuery = mock(() => {
|
|
15
|
+
const hasData = Boolean(queryResult.data);
|
|
16
|
+
return {
|
|
17
|
+
data: queryResult.data,
|
|
18
|
+
error: queryResult.error,
|
|
19
|
+
isError: queryResult.isError ?? Boolean(queryResult.error),
|
|
20
|
+
isFetching: queryResult.isFetching ?? false,
|
|
21
|
+
isLoading: queryResult.isLoading ?? false,
|
|
22
|
+
isSuccess: queryResult.isSuccess ?? hasData,
|
|
23
|
+
refetch,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
const api = {
|
|
27
|
+
injectEndpoints: mock((opts) => {
|
|
28
|
+
const builder = {
|
|
29
|
+
query: (def) => {
|
|
30
|
+
def.query();
|
|
31
|
+
return { useQuery: useTerrenoFlagConfigurationQuery };
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
opts.endpoints(builder);
|
|
35
|
+
return { useTerrenoFlagConfigurationQuery };
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
return { api, refetch, useTerrenoFlagConfigurationQuery };
|
|
39
|
+
};
|
|
40
|
+
describe("useTerrenoFeatureFlags", () => {
|
|
41
|
+
beforeEach(async () => {
|
|
42
|
+
await OpenFeature.setProviderAndWait("feature-flags", NOOP_PROVIDER);
|
|
43
|
+
await OpenFeature.setProviderAndWait("custom-ff", NOOP_PROVIDER);
|
|
44
|
+
});
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
await OpenFeature.setProviderAndWait("feature-flags", NOOP_PROVIDER);
|
|
47
|
+
await OpenFeature.setProviderAndWait("custom-ff", NOOP_PROVIDER);
|
|
48
|
+
});
|
|
49
|
+
it("sets TypedInMemoryProvider and exposes client values after success", async () => {
|
|
50
|
+
const { api } = buildApi({ data: { alpha: boolDef("on") } });
|
|
51
|
+
const { result } = renderHook(() => useTerrenoFeatureFlags(api, { domain: "feature-flags", userId: "u1" }));
|
|
52
|
+
await waitFor(() => {
|
|
53
|
+
expect(result.current.isLoading).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
expect(result.current.client.getBooleanValue("alpha", false)).toBe(true);
|
|
56
|
+
expect(result.current.flags.alpha?.defaultVariant).toBe("on");
|
|
57
|
+
});
|
|
58
|
+
it("does not leave loading stuck when skip is true", () => {
|
|
59
|
+
const { api } = buildApi({ data: { alpha: boolDef("on") } });
|
|
60
|
+
const { result } = renderHook(() => useTerrenoFeatureFlags(api, { domain: "feature-flags", skip: true, userId: "u1" }));
|
|
61
|
+
expect(result.current.isLoading).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
it("does not leave loading stuck when the flag configuration query errors", () => {
|
|
64
|
+
const { api } = buildApi({
|
|
65
|
+
error: new Error("request failed"),
|
|
66
|
+
isError: true,
|
|
67
|
+
isLoading: false,
|
|
68
|
+
isSuccess: false,
|
|
69
|
+
});
|
|
70
|
+
const { result } = renderHook(() => useTerrenoFeatureFlags(api, { domain: "feature-flags", userId: "u1" }));
|
|
71
|
+
expect(result.current.isLoading).toBe(false);
|
|
72
|
+
expect(result.current.error).toBeDefined();
|
|
73
|
+
});
|
|
74
|
+
it("isolates domains so a custom domain does not read another domain's provider", async () => {
|
|
75
|
+
const { api } = buildApi({ data: { alpha: boolDef("on") } });
|
|
76
|
+
const { result } = renderHook(() => useTerrenoFeatureFlags(api, { domain: "custom-ff", userId: "u1" }));
|
|
77
|
+
await waitFor(() => {
|
|
78
|
+
expect(result.current.isLoading).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
const defaultClient = OpenFeature.getClient();
|
|
81
|
+
expect(defaultClient.getBooleanValue("alpha", false)).toBe(false);
|
|
82
|
+
expect(result.current.client.getBooleanValue("alpha", false)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
it("refetches when the socket receives the configured event", async () => {
|
|
85
|
+
const socket = new EventEmitter();
|
|
86
|
+
const { api, refetch } = buildApi({ data: { alpha: boolDef("off") } });
|
|
87
|
+
const { result } = renderHook(() => useTerrenoFeatureFlags(api, {
|
|
88
|
+
domain: "feature-flags",
|
|
89
|
+
socket: {
|
|
90
|
+
off: (ev, fn) => {
|
|
91
|
+
socket.off(ev, fn);
|
|
92
|
+
},
|
|
93
|
+
on: (ev, fn) => {
|
|
94
|
+
socket.on(ev, fn);
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
socketEventName: "featureFlagsChanged",
|
|
98
|
+
userId: "u1",
|
|
99
|
+
}));
|
|
100
|
+
await waitFor(() => {
|
|
101
|
+
expect(result.current.isLoading).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
socket.emit("featureFlagsChanged");
|
|
104
|
+
expect(refetch).toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
it("survives React StrictMode double-mount without throwing", async () => {
|
|
107
|
+
const { api } = buildApi({ data: { alpha: boolDef("on") } });
|
|
108
|
+
const { result } = renderHook(() => useTerrenoFeatureFlags(api, { domain: "feature-flags", userId: "u1" }), {
|
|
109
|
+
wrapper: ({ children }) => React.createElement(StrictMode, null, children),
|
|
110
|
+
});
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
expect(result.current.isLoading).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
expect(result.current.client.getBooleanValue("alpha", false)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it("re-applies provider when fetched configuration changes", async () => {
|
|
117
|
+
const built = buildApi({ data: { alpha: boolDef("off") } });
|
|
118
|
+
const { result, rerender } = renderHook(({ data }) => {
|
|
119
|
+
built.useTerrenoFlagConfigurationQuery.mockImplementation(() => ({
|
|
120
|
+
data,
|
|
121
|
+
error: undefined,
|
|
122
|
+
isError: false,
|
|
123
|
+
isFetching: false,
|
|
124
|
+
isLoading: false,
|
|
125
|
+
isSuccess: true,
|
|
126
|
+
refetch: built.refetch,
|
|
127
|
+
}));
|
|
128
|
+
return useTerrenoFeatureFlags(built.api, { domain: "feature-flags", userId: "u1" });
|
|
129
|
+
}, { initialProps: { data: { alpha: boolDef("off") } } });
|
|
130
|
+
await waitFor(() => {
|
|
131
|
+
expect(result.current.client.getBooleanValue("alpha", false)).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
rerender({ data: { alpha: boolDef("on") } });
|
|
134
|
+
await waitFor(() => {
|
|
135
|
+
expect(result.current.client.getBooleanValue("alpha", false)).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe("useTerrenoFeatureFlags ref-count cleanup", () => {
|
|
140
|
+
/** Dedicated domain so parallel test files do not hold extra `feature-flags` hook refs. */
|
|
141
|
+
const refcountTestDomain = "terreno-refcount-test-isolation";
|
|
142
|
+
beforeEach(async () => {
|
|
143
|
+
await OpenFeature.setProviderAndWait(refcountTestDomain, NOOP_PROVIDER);
|
|
144
|
+
});
|
|
145
|
+
afterEach(async () => {
|
|
146
|
+
await OpenFeature.setProviderAndWait(refcountTestDomain, NOOP_PROVIDER);
|
|
147
|
+
});
|
|
148
|
+
it("installs NOOP provider after the last hook instance unmounts", async () => {
|
|
149
|
+
const { api } = buildApi({ data: { alpha: boolDef("on") } });
|
|
150
|
+
const { unmount } = renderHook(() => useTerrenoFeatureFlags(api, { domain: refcountTestDomain, userId: "u1" }));
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
expect(OpenFeature.getClient(refcountTestDomain).getBooleanValue("alpha", false)).toBe(true);
|
|
153
|
+
}, { timeout: 5000 });
|
|
154
|
+
unmount();
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(OpenFeature.getClient(refcountTestDomain).getBooleanValue("alpha", false)).toBe(false);
|
|
157
|
+
}, { timeout: 5000 });
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
//# sourceMappingURL=useTerrenoFeatureFlags.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTerrenoFeatureFlags.test.js","sourceRoot":"","sources":["../src/useTerrenoFeatureFlags.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
3
|
"@better-auth/expo": "^1.2.8",
|
|
4
|
+
"@openfeature/core": "^1.11.0",
|
|
4
5
|
"@react-native-async-storage/async-storage": "2.2.0",
|
|
5
6
|
"@reduxjs/toolkit": "^2.11.1",
|
|
6
|
-
"@terreno/ui": "0.
|
|
7
|
+
"@terreno/ui": "0.20.0",
|
|
7
8
|
"async-mutex": "^0.5.0",
|
|
8
9
|
"axios": "^1.13.2",
|
|
9
10
|
"axios-retry": "^4.5.0",
|
|
@@ -22,6 +23,8 @@
|
|
|
22
23
|
"description": "Redux Toolkit Query utilities for @terreno/api backends",
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@biomejs/biome": "^2.3.6",
|
|
26
|
+
"@openfeature/react-sdk": "^1.4.0",
|
|
27
|
+
"@openfeature/web-sdk": "^1.9.0",
|
|
25
28
|
"@testing-library/react-native": "^13.2.0",
|
|
26
29
|
"@types/bun": "^1.2.4",
|
|
27
30
|
"@types/luxon": "^3.7.1",
|
|
@@ -50,6 +53,8 @@
|
|
|
50
53
|
"main": "dist/index.js",
|
|
51
54
|
"name": "@terreno/rtk",
|
|
52
55
|
"peerDependencies": {
|
|
56
|
+
"@openfeature/react-sdk": "^1.4.0",
|
|
57
|
+
"@openfeature/web-sdk": "^1.9.0",
|
|
53
58
|
"react": "19.1.0"
|
|
54
59
|
},
|
|
55
60
|
"publishConfig": {
|
|
@@ -67,5 +72,5 @@
|
|
|
67
72
|
"test:coverage": "bun run ../scripts/check-coverage.ts"
|
|
68
73
|
},
|
|
69
74
|
"types": "dist/index.d.ts",
|
|
70
|
-
"version": "0.
|
|
75
|
+
"version": "0.20.0"
|
|
71
76
|
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it} from "bun:test";
|
|
2
|
+
import {OpenFeatureProvider, useBooleanFlagValue} from "@openfeature/react-sdk";
|
|
3
|
+
import {NOOP_PROVIDER, OpenFeature, TypedInMemoryProvider} from "@openfeature/web-sdk";
|
|
4
|
+
import {renderHook, waitFor} from "@testing-library/react-native";
|
|
5
|
+
import React from "react";
|
|
6
|
+
|
|
7
|
+
const SPIKE_DOMAIN = "openfeature-rn-spike";
|
|
8
|
+
|
|
9
|
+
describe("@openfeature/react-sdk with React Native test renderer", () => {
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
await OpenFeature.setProviderAndWait(
|
|
12
|
+
SPIKE_DOMAIN,
|
|
13
|
+
new TypedInMemoryProvider({
|
|
14
|
+
"demo-flag": {
|
|
15
|
+
defaultVariant: "on",
|
|
16
|
+
disabled: false,
|
|
17
|
+
variants: {off: false, on: true},
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await OpenFeature.setProviderAndWait(SPIKE_DOMAIN, NOOP_PROVIDER);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("exposes useBooleanFlagValue when wrapped by OpenFeatureProvider", async () => {
|
|
28
|
+
const {result} = renderHook(() => useBooleanFlagValue("demo-flag", false), {
|
|
29
|
+
wrapper: ({children}: {children: React.ReactNode}) =>
|
|
30
|
+
React.createElement(OpenFeatureProvider, {domain: SPIKE_DOMAIN}, children),
|
|
31
|
+
});
|
|
32
|
+
await waitFor(() => {
|
|
33
|
+
expect(result.current).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import {afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {NOOP_PROVIDER, OpenFeature} from "@openfeature/web-sdk";
|
|
3
|
+
import {renderHook, waitFor} from "@testing-library/react-native";
|
|
3
4
|
|
|
4
5
|
import {resolveFeatureFlagsOptions, useFeatureFlags} from "./useFeatureFlags";
|
|
5
6
|
|
|
6
|
-
type
|
|
7
|
+
type TerrenoFlagConfiguration = Record<
|
|
8
|
+
string,
|
|
9
|
+
{
|
|
10
|
+
defaultVariant: string;
|
|
11
|
+
disabled: boolean;
|
|
12
|
+
variants: Record<string, boolean | string>;
|
|
13
|
+
}
|
|
14
|
+
>;
|
|
15
|
+
|
|
7
16
|
type QueryResult = {
|
|
8
|
-
data?:
|
|
17
|
+
data?: TerrenoFlagConfiguration;
|
|
9
18
|
error?: unknown;
|
|
19
|
+
isFetching?: boolean;
|
|
10
20
|
isLoading?: boolean;
|
|
21
|
+
isSuccess?: boolean;
|
|
11
22
|
};
|
|
12
23
|
|
|
13
24
|
describe("resolveFeatureFlagsOptions", () => {
|
|
14
25
|
it("applies defaults when no argument is provided", () => {
|
|
15
26
|
expect(resolveFeatureFlagsOptions()).toEqual({
|
|
16
27
|
basePath: "/feature-flags",
|
|
28
|
+
domain: "feature-flags",
|
|
17
29
|
skip: false,
|
|
18
30
|
});
|
|
19
31
|
});
|
|
@@ -21,6 +33,7 @@ describe("resolveFeatureFlagsOptions", () => {
|
|
|
21
33
|
it("applies defaults when an empty options object is provided", () => {
|
|
22
34
|
expect(resolveFeatureFlagsOptions({})).toEqual({
|
|
23
35
|
basePath: "/feature-flags",
|
|
36
|
+
domain: "feature-flags",
|
|
24
37
|
skip: false,
|
|
25
38
|
});
|
|
26
39
|
});
|
|
@@ -28,6 +41,7 @@ describe("resolveFeatureFlagsOptions", () => {
|
|
|
28
41
|
it("treats a string argument as a legacy basePath with skip=false", () => {
|
|
29
42
|
expect(resolveFeatureFlagsOptions("/custom-path")).toEqual({
|
|
30
43
|
basePath: "/custom-path",
|
|
44
|
+
domain: "feature-flags",
|
|
31
45
|
skip: false,
|
|
32
46
|
});
|
|
33
47
|
});
|
|
@@ -35,6 +49,7 @@ describe("resolveFeatureFlagsOptions", () => {
|
|
|
35
49
|
it("preserves an empty string basePath when passed as a legacy string argument", () => {
|
|
36
50
|
expect(resolveFeatureFlagsOptions("")).toEqual({
|
|
37
51
|
basePath: "",
|
|
52
|
+
domain: "feature-flags",
|
|
38
53
|
skip: false,
|
|
39
54
|
});
|
|
40
55
|
});
|
|
@@ -42,6 +57,7 @@ describe("resolveFeatureFlagsOptions", () => {
|
|
|
42
57
|
it("uses options.basePath when provided", () => {
|
|
43
58
|
expect(resolveFeatureFlagsOptions({basePath: "/flags"})).toEqual({
|
|
44
59
|
basePath: "/flags",
|
|
60
|
+
domain: "feature-flags",
|
|
45
61
|
skip: false,
|
|
46
62
|
});
|
|
47
63
|
});
|
|
@@ -49,6 +65,7 @@ describe("resolveFeatureFlagsOptions", () => {
|
|
|
49
65
|
it("uses options.skip when provided", () => {
|
|
50
66
|
expect(resolveFeatureFlagsOptions({skip: true})).toEqual({
|
|
51
67
|
basePath: "/feature-flags",
|
|
68
|
+
domain: "feature-flags",
|
|
52
69
|
skip: true,
|
|
53
70
|
});
|
|
54
71
|
});
|
|
@@ -56,6 +73,7 @@ describe("resolveFeatureFlagsOptions", () => {
|
|
|
56
73
|
it("uses both options.basePath and options.skip when provided together", () => {
|
|
57
74
|
expect(resolveFeatureFlagsOptions({basePath: "/flags", skip: true})).toEqual({
|
|
58
75
|
basePath: "/flags",
|
|
76
|
+
domain: "feature-flags",
|
|
59
77
|
skip: true,
|
|
60
78
|
});
|
|
61
79
|
});
|
|
@@ -65,69 +83,120 @@ describe("resolveFeatureFlagsOptions", () => {
|
|
|
65
83
|
});
|
|
66
84
|
});
|
|
67
85
|
|
|
86
|
+
const boolDef = (variant: "on" | "off") => ({
|
|
87
|
+
defaultVariant: variant,
|
|
88
|
+
disabled: false,
|
|
89
|
+
variants: {off: false, on: true},
|
|
90
|
+
});
|
|
91
|
+
|
|
68
92
|
const buildApi = (queryResult: QueryResult) => {
|
|
69
93
|
const refetch = mock(() => {});
|
|
70
94
|
const capturedQueryBuilder: Array<{method: string; url: string}> = [];
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
const useTerrenoFlagConfigurationQuery = mock(() => {
|
|
96
|
+
const hasData = Boolean(queryResult.data);
|
|
97
|
+
return {
|
|
98
|
+
data: queryResult.data,
|
|
99
|
+
error: queryResult.error,
|
|
100
|
+
isFetching: queryResult.isFetching ?? false,
|
|
101
|
+
isLoading: queryResult.isLoading ?? false,
|
|
102
|
+
isSuccess: queryResult.isSuccess ?? hasData,
|
|
103
|
+
refetch,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
77
106
|
const api = {
|
|
78
107
|
injectEndpoints: mock((opts: {endpoints: (builder: unknown) => void}) => {
|
|
79
108
|
const builder = {
|
|
80
109
|
query: (def: {query: () => {method: string; url: string}}) => {
|
|
81
110
|
capturedQueryBuilder.push(def.query());
|
|
82
|
-
return {useQuery:
|
|
111
|
+
return {useQuery: useTerrenoFlagConfigurationQuery};
|
|
83
112
|
},
|
|
84
113
|
};
|
|
85
114
|
opts.endpoints(builder);
|
|
86
|
-
return {
|
|
115
|
+
return {useTerrenoFlagConfigurationQuery};
|
|
87
116
|
}),
|
|
88
117
|
};
|
|
89
|
-
return {api, capturedQueryBuilder, refetch,
|
|
118
|
+
return {api, capturedQueryBuilder, refetch, useTerrenoFlagConfigurationQuery};
|
|
90
119
|
};
|
|
91
120
|
|
|
92
121
|
describe("useFeatureFlags hook", () => {
|
|
93
122
|
const debugCalls: unknown[][] = [];
|
|
94
123
|
const originalDebug = console.debug;
|
|
95
124
|
|
|
96
|
-
beforeEach(() => {
|
|
125
|
+
beforeEach(async () => {
|
|
97
126
|
debugCalls.length = 0;
|
|
98
127
|
console.debug = (...args: unknown[]): void => {
|
|
99
128
|
debugCalls.push(args);
|
|
100
129
|
};
|
|
130
|
+
await OpenFeature.setProviderAndWait("feature-flags", NOOP_PROVIDER);
|
|
101
131
|
});
|
|
102
132
|
|
|
103
|
-
afterEach(() => {
|
|
133
|
+
afterEach(async () => {
|
|
104
134
|
console.debug = originalDebug;
|
|
135
|
+
await OpenFeature.setProviderAndWait("feature-flags", NOOP_PROVIDER);
|
|
105
136
|
});
|
|
106
137
|
|
|
107
|
-
it("builds the
|
|
108
|
-
const {api, capturedQueryBuilder} = buildApi({data: {foo:
|
|
109
|
-
renderHook(() =>
|
|
138
|
+
it("builds the flagConfiguration endpoint with the default basePath", () => {
|
|
139
|
+
const {api, capturedQueryBuilder} = buildApi({data: {foo: boolDef("on")}});
|
|
140
|
+
renderHook(() =>
|
|
141
|
+
useFeatureFlags(api as unknown as Parameters<typeof useFeatureFlags>[0], {
|
|
142
|
+
userId: "u1",
|
|
143
|
+
})
|
|
144
|
+
);
|
|
110
145
|
expect(capturedQueryBuilder[0]).toEqual({
|
|
111
146
|
method: "GET",
|
|
112
|
-
url: "/feature-flags/
|
|
147
|
+
url: "/feature-flags/flagConfiguration",
|
|
113
148
|
});
|
|
114
149
|
});
|
|
115
150
|
|
|
116
|
-
it("builds the
|
|
117
|
-
const {api, capturedQueryBuilder} = buildApi({data: {foo:
|
|
151
|
+
it("builds the flagConfiguration endpoint with a custom basePath from legacy string argument", () => {
|
|
152
|
+
const {api, capturedQueryBuilder} = buildApi({data: {foo: boolDef("on")}});
|
|
118
153
|
renderHook(() =>
|
|
119
154
|
useFeatureFlags(api as unknown as Parameters<typeof useFeatureFlags>[0], "/flags")
|
|
120
155
|
);
|
|
121
|
-
expect(capturedQueryBuilder[0]).toEqual({method: "GET", url: "/flags/
|
|
156
|
+
expect(capturedQueryBuilder[0]).toEqual({method: "GET", url: "/flags/flagConfiguration"});
|
|
122
157
|
});
|
|
123
158
|
|
|
124
|
-
it("
|
|
159
|
+
it("projects FlagConfiguration to flat flags like the legacy /evaluate map", async () => {
|
|
125
160
|
const {api} = buildApi({
|
|
126
|
-
data: {
|
|
161
|
+
data: {
|
|
162
|
+
b1: boolDef("on"),
|
|
163
|
+
v1: {
|
|
164
|
+
defaultVariant: "compact",
|
|
165
|
+
disabled: false,
|
|
166
|
+
variants: {compact: "compact", detailed: "detailed"},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
127
169
|
});
|
|
128
170
|
const {result} = renderHook(() =>
|
|
129
|
-
useFeatureFlags(api as unknown as Parameters<typeof useFeatureFlags>[0])
|
|
171
|
+
useFeatureFlags(api as unknown as Parameters<typeof useFeatureFlags>[0], {userId: "u1"})
|
|
130
172
|
);
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(result.current.isLoading).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
expect(result.current.flags).toEqual({
|
|
177
|
+
b1: true,
|
|
178
|
+
v1: "compact",
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("returns flag accessors that read boolean and string values", async () => {
|
|
183
|
+
const {api} = buildApi({
|
|
184
|
+
data: {
|
|
185
|
+
booleanOff: boolDef("off"),
|
|
186
|
+
booleanOn: boolDef("on"),
|
|
187
|
+
variantFlag: {
|
|
188
|
+
defaultVariant: "variant-a",
|
|
189
|
+
disabled: false,
|
|
190
|
+
variants: {compact: "compact", detailed: "detailed", "variant-a": "variant-a"},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
const {result} = renderHook(() =>
|
|
195
|
+
useFeatureFlags(api as unknown as Parameters<typeof useFeatureFlags>[0], {userId: "u1"})
|
|
196
|
+
);
|
|
197
|
+
await waitFor(() => {
|
|
198
|
+
expect(result.current.isLoading).toBe(false);
|
|
199
|
+
});
|
|
131
200
|
expect(result.current.getFlag("booleanOn")).toBe(true);
|
|
132
201
|
expect(result.current.getFlag("booleanOff")).toBe(false);
|
|
133
202
|
expect(result.current.getFlag("variantFlag")).toBe(false);
|
|
@@ -137,98 +206,115 @@ describe("useFeatureFlags hook", () => {
|
|
|
137
206
|
});
|
|
138
207
|
|
|
139
208
|
it("returns an empty flags map when no data is available", () => {
|
|
140
|
-
const {api} = buildApi({isLoading: true});
|
|
209
|
+
const {api} = buildApi({isLoading: true, isSuccess: false});
|
|
141
210
|
const {result} = renderHook(() =>
|
|
142
|
-
useFeatureFlags(api as unknown as Parameters<typeof useFeatureFlags>[0])
|
|
211
|
+
useFeatureFlags(api as unknown as Parameters<typeof useFeatureFlags>[0], {userId: "u1"})
|
|
143
212
|
);
|
|
144
213
|
expect(result.current.flags).toEqual({});
|
|
145
214
|
expect(result.current.isLoading).toBe(true);
|
|
146
215
|
});
|
|
147
216
|
|
|
148
|
-
it("exposes refetch from the underlying query hook", () => {
|
|
149
|
-
const {api, refetch} = buildApi({data: {foo:
|
|
217
|
+
it("exposes refetch from the underlying query hook", async () => {
|
|
218
|
+
const {api, refetch} = buildApi({data: {foo: boolDef("on")}});
|
|
150
219
|
const {result} = renderHook(() =>
|
|
151
|
-
useFeatureFlags(api as unknown as Parameters<typeof useFeatureFlags>[0])
|
|
220
|
+
useFeatureFlags(api as unknown as Parameters<typeof useFeatureFlags>[0], {userId: "u1"})
|
|
152
221
|
);
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
expect(result.current.isLoading).toBe(false);
|
|
224
|
+
});
|
|
153
225
|
result.current.refetch();
|
|
154
226
|
expect(refetch).toHaveBeenCalled();
|
|
155
227
|
});
|
|
156
228
|
|
|
157
|
-
it("logs
|
|
158
|
-
const {api} = buildApi({isLoading: true});
|
|
159
|
-
renderHook(() =>
|
|
229
|
+
it("logs flagConfiguration request started when loading begins without prior data", () => {
|
|
230
|
+
const {api} = buildApi({isLoading: true, isSuccess: false});
|
|
231
|
+
renderHook(() =>
|
|
232
|
+
useFeatureFlags(api as unknown as Parameters<typeof useFeatureFlags>[0], {userId: "u1"})
|
|
233
|
+
);
|
|
160
234
|
const started = debugCalls.find(
|
|
161
|
-
(args) => args[0] === "[feature-flags]
|
|
235
|
+
(args) => args[0] === "[feature-flags] flagConfiguration request started"
|
|
162
236
|
);
|
|
163
237
|
expect(started).toBeDefined();
|
|
164
238
|
});
|
|
165
239
|
|
|
166
|
-
it("logs
|
|
167
|
-
const api = buildApi({isLoading: true});
|
|
240
|
+
it("logs flagConfiguration request completed when data resolves after a loading phase", async () => {
|
|
241
|
+
const api = buildApi({isLoading: true, isSuccess: false});
|
|
168
242
|
const {rerender} = renderHook(
|
|
169
243
|
({queryResult}: {queryResult: QueryResult}) => {
|
|
170
|
-
api.
|
|
244
|
+
api.useTerrenoFlagConfigurationQuery.mockImplementationOnce(() => ({
|
|
171
245
|
data: queryResult.data,
|
|
172
246
|
error: queryResult.error,
|
|
247
|
+
isFetching: queryResult.isFetching ?? false,
|
|
173
248
|
isLoading: queryResult.isLoading ?? false,
|
|
249
|
+
isSuccess: queryResult.isSuccess ?? Boolean(queryResult.data),
|
|
174
250
|
refetch: api.refetch,
|
|
175
251
|
}));
|
|
176
|
-
return useFeatureFlags(api.api as unknown as Parameters<typeof useFeatureFlags>[0]
|
|
252
|
+
return useFeatureFlags(api.api as unknown as Parameters<typeof useFeatureFlags>[0], {
|
|
253
|
+
userId: "u1",
|
|
254
|
+
});
|
|
177
255
|
},
|
|
178
|
-
{initialProps: {queryResult: {isLoading: true}}}
|
|
179
|
-
);
|
|
180
|
-
rerender({queryResult: {data: {alpha: true}, isLoading: false}});
|
|
181
|
-
const completed = debugCalls.find(
|
|
182
|
-
(args) => args[0] === "[feature-flags] evaluate request completed"
|
|
256
|
+
{initialProps: {queryResult: {isLoading: true, isSuccess: false}}}
|
|
183
257
|
);
|
|
258
|
+
rerender({queryResult: {data: {alpha: boolDef("on")}, isLoading: false, isSuccess: true}});
|
|
259
|
+
await waitFor(() => {
|
|
260
|
+
const completed = debugCalls.find(
|
|
261
|
+
(args) => args[0] === "[feature-flags] flagConfiguration request completed"
|
|
262
|
+
);
|
|
263
|
+
expect(completed).toBeDefined();
|
|
264
|
+
});
|
|
184
265
|
const started = debugCalls.find(
|
|
185
|
-
(args) => args[0] === "[feature-flags]
|
|
266
|
+
(args) => args[0] === "[feature-flags] flagConfiguration request started"
|
|
186
267
|
);
|
|
187
268
|
expect(started).toBeDefined();
|
|
188
|
-
expect(completed).toBeDefined();
|
|
189
269
|
});
|
|
190
270
|
|
|
191
|
-
it("injects the
|
|
271
|
+
it("injects the flagConfiguration endpoint only once per (api, basePath)", () => {
|
|
192
272
|
let injectCallCount = 0;
|
|
193
273
|
const refetch = mock(() => {});
|
|
194
|
-
const
|
|
274
|
+
const useTerrenoFlagConfigurationQuery = mock(() => ({
|
|
195
275
|
data: {},
|
|
196
276
|
error: undefined,
|
|
277
|
+
isFetching: false,
|
|
197
278
|
isLoading: false,
|
|
279
|
+
isSuccess: true,
|
|
198
280
|
refetch,
|
|
199
281
|
}));
|
|
200
282
|
const api = {
|
|
201
283
|
injectEndpoints: () => {
|
|
202
284
|
injectCallCount += 1;
|
|
203
|
-
return {
|
|
285
|
+
return {useTerrenoFlagConfigurationQuery};
|
|
204
286
|
},
|
|
205
287
|
};
|
|
206
288
|
const {rerender} = renderHook(() =>
|
|
207
|
-
useFeatureFlags(api as unknown as Parameters<typeof useFeatureFlags>[0])
|
|
289
|
+
useFeatureFlags(api as unknown as Parameters<typeof useFeatureFlags>[0], {userId: "u1"})
|
|
208
290
|
);
|
|
209
291
|
rerender(undefined);
|
|
210
292
|
rerender(undefined);
|
|
211
|
-
// The hook reuses the cached enhanced api after the first render so the
|
|
212
|
-
// dev-mode RTK warning about re-injecting endpoints never fires.
|
|
213
293
|
expect(injectCallCount).toBe(1);
|
|
214
294
|
});
|
|
215
295
|
|
|
216
|
-
it("logs
|
|
217
|
-
const api = buildApi({isLoading: true});
|
|
296
|
+
it("logs flagConfiguration request failed when error is returned after loading", () => {
|
|
297
|
+
const api = buildApi({isLoading: true, isSuccess: false});
|
|
218
298
|
const {rerender} = renderHook(
|
|
219
299
|
({queryResult}: {queryResult: QueryResult}) => {
|
|
220
|
-
api.
|
|
300
|
+
api.useTerrenoFlagConfigurationQuery.mockImplementationOnce(() => ({
|
|
221
301
|
data: queryResult.data,
|
|
222
302
|
error: queryResult.error,
|
|
303
|
+
isFetching: queryResult.isFetching ?? false,
|
|
223
304
|
isLoading: queryResult.isLoading ?? false,
|
|
305
|
+
isSuccess: queryResult.isSuccess ?? false,
|
|
224
306
|
refetch: api.refetch,
|
|
225
307
|
}));
|
|
226
|
-
return useFeatureFlags(api.api as unknown as Parameters<typeof useFeatureFlags>[0]
|
|
308
|
+
return useFeatureFlags(api.api as unknown as Parameters<typeof useFeatureFlags>[0], {
|
|
309
|
+
userId: "u1",
|
|
310
|
+
});
|
|
227
311
|
},
|
|
228
|
-
{initialProps: {queryResult: {isLoading: true}}}
|
|
312
|
+
{initialProps: {queryResult: {isLoading: true, isSuccess: false}}}
|
|
313
|
+
);
|
|
314
|
+
rerender({queryResult: {error: new Error("boom"), isLoading: false, isSuccess: false}});
|
|
315
|
+
const failed = debugCalls.find(
|
|
316
|
+
(args) => args[0] === "[feature-flags] flagConfiguration request failed"
|
|
229
317
|
);
|
|
230
|
-
rerender({queryResult: {error: new Error("boom"), isLoading: false}});
|
|
231
|
-
const failed = debugCalls.find((args) => args[0] === "[feature-flags] evaluate request failed");
|
|
232
318
|
expect(failed).toBeDefined();
|
|
233
319
|
});
|
|
234
320
|
});
|