@configdirector/react-native-sdk 1.5.0 → 1.6.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 CHANGED
@@ -32,6 +32,49 @@ function Home() {
32
32
 
33
33
  Full details are in the [official documentation](https://docs.configdirector.com/sdks/mobile/react-native).
34
34
 
35
+ ## Test your code
36
+
37
+ `@configdirector/react-native-sdk/testing` creates a real client connected to an in-memory server that
38
+ your test controls. Render the production `ConfigDirectorProvider` with its `client` prop, or install
39
+ the test client for components that render their own provider, such as `App`.
40
+
41
+ ```tsx
42
+ import { act, render } from "@testing-library/react-native";
43
+ import { ConfigDirectorProvider } from "@configdirector/react-native-sdk";
44
+ import { createTestClient, installTestClient } from "@configdirector/react-native-sdk/testing";
45
+
46
+ const testClient = createTestClient({ values: { "dark-mode": true } });
47
+
48
+ const view = await render(
49
+ <ConfigDirectorProvider client={testClient.client}>
50
+ <Home />
51
+ </ConfigDirectorProvider>,
52
+ );
53
+ await view.findByText("dark-mode: true");
54
+
55
+ await act(() => testClient.setValue("dark-mode", false));
56
+ view.getByText("dark-mode: false");
57
+
58
+ const uninstall = installTestClient(testClient);
59
+ await render(<App />);
60
+ uninstall();
61
+ ```
62
+
63
+ The provider renders its children before initialization completes, so the first assertion uses
64
+ `findBy…`. Wrap `setValue`, `removeValue`, and `replaceValues` in `act()`. React Native Testing
65
+ Library 14 made `render`, `fireEvent`, and `act` asynchronous, so the example awaits them; the same
66
+ code works with version 13. The test client also holds
67
+ or fails `initialize` and `updateContext`, and records every context in `contextUpdates`. A provider
68
+ never disposes a client it was given, removes its handlers and its `AppState` and NetInfo
69
+ subscriptions when it unmounts, and does not initialize a client that is ready or initializing.
70
+ Under the React Native Jest preset, `AppState.addEventListener` never calls its listener, so the
71
+ provider never pauses on its own; a test that calls the listener runs the production pause and
72
+ resume against the test client. The package and its dependency `@noble/hashes` are ESM, and the
73
+ package's files are `.mjs` files, which the `jest-expo` and `@react-native/jest-preset` presets do not
74
+ transform, so a Jest setup must let `transformIgnorePatterns` transform both packages and add a
75
+ `transform` entry for `.mjs` files; the documentation shows the configuration for both presets.
76
+ `jest-expo` requires `@react-native/jest-preset` as a peer dependency, so install it in either case.
77
+
35
78
  ## Documentation
36
79
 
37
80
  Refer to the [official documentation for the React Native SDK](https://docs.configdirector.com/sdks/mobile/react-native).
@@ -0,0 +1,157 @@
1
+ import { i as ConfigDirectorContext, n as ConfigDirectorClient, o as ConfigDirectorLogger } from "./types-fcPMBgiX.mjs";
2
+
3
+ //#region ../shared/src/errors.d.ts
4
+ declare class ConfigDirectorConnectionError extends Error {
5
+ readonly name: string;
6
+ readonly status?: number;
7
+ constructor(message: string, status?: number);
8
+ }
9
+ declare class ConfigDirectorValidationError extends Error {
10
+ readonly name: string;
11
+ constructor(message: string);
12
+ }
13
+ //#endregion
14
+ //#region ../shared/src/testing/values.d.ts
15
+ type TestJsonValue = boolean | number | string | null | TestJsonValue[] | {
16
+ [key: string]: TestJsonValue;
17
+ };
18
+ type TestValue = boolean | number | string | TestJsonValue[] | {
19
+ [key: string]: TestJsonValue;
20
+ };
21
+ type TestValues = Record<string, TestValue>;
22
+ //#endregion
23
+ //#region ../js-client-core/src/testing/TestClient.d.ts
24
+ /**
25
+ * Options for {@link createTestClient}. Every option is optional.
26
+ */
27
+ type TestClientOptions = {
28
+ /**
29
+ * The config values the test client serves once its client connects, keyed by config key. The
30
+ * config type follows from each value: a boolean, an integral number (`integer`), any other
31
+ * number (`float`), a string, or a plain object or array (`json`).
32
+ */
33
+ values?: TestValues;
34
+ /**
35
+ * The connection timeout of the SDK client, in milliseconds. It bounds how long a held
36
+ * `initialize` or `updateContext` waits. Defaults to the SDK's production timeout.
37
+ */
38
+ timeout?: number;
39
+ /**
40
+ * The logger the SDK client uses. Defaults to the SDK's console logger at `warn` level.
41
+ */
42
+ logger?: ConfigDirectorLogger;
43
+ };
44
+ /**
45
+ * A real ConfigDirector client connected to an in-memory server that the test controls.
46
+ *
47
+ * The client under {@link TestClient.client} is the SDK's production client: value parsing, watchers,
48
+ * events, and readiness behave as they do against ConfigDirector. Only the network connection and
49
+ * telemetry are replaced, so no request is ever made and nothing is left running after the client is
50
+ * disposed.
51
+ */
52
+ interface TestClient {
53
+ /**
54
+ * The SDK client to hand to the code under test. It starts uninitialized, like a production
55
+ * client; `initialize` completes at once with the seeded values unless initialization is held.
56
+ */
57
+ readonly client: ConfigDirectorClient;
58
+ /**
59
+ * Stores `value` under `key` and, when the client is connected, delivers it as an update, so
60
+ * reads, watchers, and `configsUpdated` handlers see it.
61
+ */
62
+ setValue(key: string, value: TestValue): void;
63
+ /**
64
+ * Removes `key` and, when the client is connected, delivers a full update without it, so reads
65
+ * fall back to the in-code default value and watchers of `key` receive that default.
66
+ */
67
+ removeValue(key: string): void;
68
+ /**
69
+ * Replaces every stored value with `values`, disarms any armed hold or failure, and, when the
70
+ * client is connected, delivers the new values as a full update.
71
+ */
72
+ replaceValues(values: TestValues): void;
73
+ /**
74
+ * Makes the next `initialize` wait until {@link completeInitialization} or
75
+ * {@link failInitialization} is called, or until the client's timeout elapses.
76
+ */
77
+ holdInitialization(): void;
78
+ /**
79
+ * Delivers the stored values to a held `initialize`, so it completes with the client ready. When
80
+ * a hold is armed but no `initialize` has started, it disarms the hold instead.
81
+ */
82
+ completeInitialization(): void;
83
+ /**
84
+ * Fails a held `initialize` with an unrecoverable connection error, or arms the next `initialize`
85
+ * to fail. `initialize` completes promptly with the client not ready and `connectionError` fires.
86
+ */
87
+ failInitialization(): void;
88
+ /**
89
+ * Makes the next `updateContext` wait until {@link completeContextUpdate} or
90
+ * {@link failContextUpdate} is called, or until the client's timeout elapses.
91
+ */
92
+ holdContextUpdate(): void;
93
+ /**
94
+ * Delivers the stored values to a held `updateContext`, or disarms an armed hold that no
95
+ * `updateContext` has picked up.
96
+ */
97
+ completeContextUpdate(): void;
98
+ /**
99
+ * Fails a held `updateContext` with an unrecoverable connection error, or arms the next
100
+ * `updateContext` to fail.
101
+ */
102
+ failContextUpdate(): void;
103
+ /**
104
+ * The context of every `initialize` and `updateContext` call, in call order. `initialize` without
105
+ * a context records an empty context. Resumes are not recorded.
106
+ */
107
+ readonly contextUpdates: ConfigDirectorContext[];
108
+ }
109
+ //#endregion
110
+ //#region ../js-client-core/src/testing/installTestClient.d.ts
111
+ /**
112
+ * Makes every `ConfigDirectorProvider` mounted without a `client` prop, and every client
113
+ * `withProvider` would build, use `testClient.client` instead of building a client, until the
114
+ * returned function uninstalls it. Construction props such as `sdkKey` are ignored while installed;
115
+ * `context` and `hooks` apply to the installed client as they do for a `client` prop. A provider
116
+ * reads the installed client when it is constructed, so install before rendering.
117
+ *
118
+ * Installing again replaces the installed test client. Each uninstall function restores the test
119
+ * client that was installed when it was created, so nested installs are uninstalled in reverse order.
120
+ *
121
+ * @param testClient The test client whose `client` providers should use
122
+ * @returns A function that uninstalls it
123
+ *
124
+ * @example
125
+ * let uninstallTestClient: () => void;
126
+ * beforeEach(() => {
127
+ * testClient = createTestClient({ values: { "new-checkout": true } });
128
+ * uninstallTestClient = installTestClient(testClient);
129
+ * });
130
+ * afterEach(() => uninstallTestClient());
131
+ */
132
+ declare const installTestClient: (testClient: TestClient) => (() => void);
133
+ //#endregion
134
+ //#region src/testing.d.ts
135
+ /**
136
+ * Creates a {@link TestClient}: a real `ConfigDirectorClient` connected to an in-memory server
137
+ * that the test controls. No network connection is opened and no telemetry is sent. Render with
138
+ * `<ConfigDirectorProvider client={testClient.client}>`, or install it with `installTestClient` for
139
+ * components that render their own provider.
140
+ *
141
+ * @param options {@link TestClientOptions}: the initial values, the client's timeout, and its logger
142
+ * @returns A {@link TestClient} whose `client` is passed to the code under test
143
+ *
144
+ * @example
145
+ * import { createTestClient } from "@configdirector/react-native-sdk/testing";
146
+ *
147
+ * const testClient = createTestClient({ values: { "new-checkout": true } });
148
+ * const view = render(
149
+ * <ConfigDirectorProvider client={testClient.client}>
150
+ * <Home />
151
+ * </ConfigDirectorProvider>,
152
+ * );
153
+ * act(() => testClient.setValue("new-checkout", false));
154
+ */
155
+ declare const createTestClient: (options?: TestClientOptions) => TestClient;
156
+ //#endregion
157
+ export { ConfigDirectorConnectionError, ConfigDirectorValidationError, type TestClient, type TestClientOptions, type TestJsonValue, type TestValue, type TestValues, createTestClient, installTestClient };
@@ -0,0 +1,398 @@
1
+ import { c as ConfigDirectorConnectionError, d as _defineProperty, l as ConfigDirectorValidationError, o as createDefaultLogger, r as DefaultConfigDirectorClient, t as INSTALLED_TEST_CLIENT_SLOT, u as Emitter } from "./installedTestClient-CxGEAMOE.mjs";
2
+ //#region ../shared/src/testing/values.ts
3
+ const encodeTestValue$1 = (key, value) => {
4
+ if (key.trim().length == 0) throw new ConfigDirectorValidationError("Invalid config key. The key of a test value cannot be empty.");
5
+ return encode(key, value);
6
+ };
7
+ const encode = (key, value) => {
8
+ switch (typeof value) {
9
+ case "boolean": return {
10
+ type: "boolean",
11
+ value: value ? "true" : "false"
12
+ };
13
+ case "number": return encodeNumber(key, value);
14
+ case "string": return {
15
+ type: "string",
16
+ value
17
+ };
18
+ case "object": if (value !== null) {
19
+ assertJsonContents(key, value, "");
20
+ return {
21
+ type: "json",
22
+ value: JSON.stringify(value)
23
+ };
24
+ }
25
+ }
26
+ throw new ConfigDirectorValidationError(`Invalid test value for '${key}': ${describe(value)}. Use a boolean, a number, a string, a plain object, or an array.`);
27
+ };
28
+ const encodeNumber = (key, value) => {
29
+ if (!Number.isFinite(value)) throw new ConfigDirectorValidationError(`Invalid test value for '${key}': ${describe(value)} is not a finite number.`);
30
+ if (Number.isInteger(value)) {
31
+ if (!Number.isSafeInteger(value)) throw new ConfigDirectorValidationError(`Invalid test value for '${key}': ${value} is not a safe integer, so the SDK could not read it back exactly.`);
32
+ return {
33
+ type: "integer",
34
+ value: String(value)
35
+ };
36
+ }
37
+ return {
38
+ type: "float",
39
+ value: toPlainDecimal(value)
40
+ };
41
+ };
42
+ const toPlainDecimal = (value) => {
43
+ const text = String(value);
44
+ const exponentIndex = text.indexOf("e-");
45
+ if (exponentIndex < 0) return text;
46
+ const sign = text.startsWith("-") ? "-" : "";
47
+ const [integerDigits = "", fractionDigits = ""] = text.slice(sign.length, exponentIndex).split(".");
48
+ const leadingZeros = Number(text.slice(exponentIndex + 2)) - integerDigits.length;
49
+ return `${sign}0.${"0".repeat(leadingZeros)}${integerDigits}${fractionDigits}`;
50
+ };
51
+ const assertJsonContents = (key, value, path) => {
52
+ if (value === null || typeof value === "boolean" || typeof value === "string") return;
53
+ if (typeof value === "number") {
54
+ if (!Number.isFinite(value)) throw invalidJsonContents(key, path, `${describe(value)} is not a finite number`);
55
+ return;
56
+ }
57
+ if (Array.isArray(value)) {
58
+ value.forEach((item, index) => assertJsonContents(key, item, `${path}[${index}]`));
59
+ return;
60
+ }
61
+ if (isPlainObject(value)) {
62
+ for (const [property, item] of Object.entries(value)) assertJsonContents(key, item, path ? `${path}.${property}` : property);
63
+ return;
64
+ }
65
+ throw invalidJsonContents(key, path, `${describe(value)} cannot be encoded as JSON`);
66
+ };
67
+ const invalidJsonContents = (key, path, problem) => new ConfigDirectorValidationError(`Invalid test value for '${key}'${path ? ` at '${path}'` : ""}: ${problem}. JSON contents can hold booleans, finite numbers, strings, null, arrays, and plain objects.`);
68
+ const isPlainObject = (value) => {
69
+ if (typeof value !== "object" || value === null) return false;
70
+ const prototype = Object.getPrototypeOf(value);
71
+ return prototype === Object.prototype || prototype === null;
72
+ };
73
+ const describe = (value) => {
74
+ if (value === null) return "null";
75
+ if (typeof value === "function") return "a function";
76
+ if (typeof value === "object") {
77
+ var _value$constructor$na, _value$constructor;
78
+ return `a ${(_value$constructor$na = (_value$constructor = value.constructor) === null || _value$constructor === void 0 ? void 0 : _value$constructor.name) !== null && _value$constructor$na !== void 0 ? _value$constructor$na : "object"} instance`;
79
+ }
80
+ if (typeof value === "symbol") return "a symbol";
81
+ return String(value);
82
+ };
83
+ //#endregion
84
+ //#region ../js-client-core/src/testing/values.ts
85
+ const encodeTestValue = (key, value, revision) => {
86
+ const encoded = encodeTestValue$1(key, value);
87
+ return {
88
+ id: `test-config:${key}`,
89
+ key,
90
+ type: encoded.type,
91
+ value: encoded.value,
92
+ valueId: `test-value:${key}:${revision}`
93
+ };
94
+ };
95
+ //#endregion
96
+ //#region ../js-client-core/src/testing/InMemoryConnection.ts
97
+ const TEST_ENVIRONMENT = {
98
+ environmentId: "test-environment",
99
+ projectId: "test-project"
100
+ };
101
+ const controlledAttemptFor = (reason) => {
102
+ switch (reason) {
103
+ case "initialization": return "initialize";
104
+ case "context update": return "updateContext";
105
+ default: return;
106
+ }
107
+ };
108
+ var InMemoryConnection = class {
109
+ constructor(logger, values) {
110
+ this.logger = logger;
111
+ _defineProperty(this, "emitter", new Emitter());
112
+ _defineProperty(this, "configs", /* @__PURE__ */ new Map());
113
+ _defineProperty(this, "recordedContexts", []);
114
+ _defineProperty(this, "pendingDeliveries", []);
115
+ _defineProperty(this, "armed", {
116
+ initialize: "nothing",
117
+ updateContext: "nothing"
118
+ });
119
+ _defineProperty(this, "pendingHold", void 0);
120
+ _defineProperty(this, "connected", false);
121
+ _defineProperty(this, "delivering", false);
122
+ _defineProperty(this, "revision", 0);
123
+ _defineProperty(this, "attemptGeneration", 0);
124
+ _defineProperty(this, "on", (name, handler) => this.emitter.on(name, handler));
125
+ _defineProperty(this, "off", (name, handler) => this.emitter.off(name, handler));
126
+ this.store(values);
127
+ }
128
+ get contextUpdates() {
129
+ return [...this.recordedContexts];
130
+ }
131
+ setValue(key, value) {
132
+ const configState = encodeTestValue(key, value, ++this.revision);
133
+ this.configs.set(key, configState);
134
+ this.deliverIfConnected({
135
+ ...TEST_ENVIRONMENT,
136
+ kind: "delta",
137
+ configs: { [key]: configState }
138
+ });
139
+ }
140
+ removeValue(key) {
141
+ this.configs.delete(key);
142
+ this.deliverIfConnected(this.fullUpdate());
143
+ }
144
+ replaceValues(values) {
145
+ const encoded = this.encodeAll(values);
146
+ this.configs.clear();
147
+ encoded.forEach((configState, key) => this.configs.set(key, configState));
148
+ this.armed.initialize = "nothing";
149
+ this.armed.updateContext = "nothing";
150
+ this.deliverIfConnected(this.fullUpdate());
151
+ }
152
+ hold(attempt) {
153
+ this.armed[attempt] = "hold";
154
+ }
155
+ complete(attempt) {
156
+ var _this$pendingHold;
157
+ if (((_this$pendingHold = this.pendingHold) === null || _this$pendingHold === void 0 ? void 0 : _this$pendingHold.attempt) === attempt) this.pendingHold.complete();
158
+ else if (this.armed[attempt] === "hold") this.armed[attempt] = "nothing";
159
+ }
160
+ fail(attempt) {
161
+ var _this$pendingHold2;
162
+ if (((_this$pendingHold2 = this.pendingHold) === null || _this$pendingHold2 === void 0 ? void 0 : _this$pendingHold2.attempt) === attempt) this.pendingHold.fail();
163
+ else this.armed[attempt] = "failure";
164
+ }
165
+ connect(context, timeout, reason) {
166
+ this.endAttempt();
167
+ const attempt = controlledAttemptFor(reason);
168
+ if (!attempt) return this.connectNow();
169
+ this.recordedContexts.push(context);
170
+ const armed = this.armed[attempt];
171
+ this.armed[attempt] = "nothing";
172
+ switch (armed) {
173
+ case "failure": return this.failNow(reason);
174
+ case "hold": return this.holdNow(attempt, reason, timeout);
175
+ default: return this.connectNow();
176
+ }
177
+ }
178
+ clear() {
179
+ this.emitter.clear();
180
+ }
181
+ close() {
182
+ this.endAttempt();
183
+ }
184
+ dispose() {
185
+ this.close();
186
+ this.clear();
187
+ }
188
+ endAttempt() {
189
+ var _this$pendingHold3;
190
+ this.attemptGeneration++;
191
+ this.connected = false;
192
+ (_this$pendingHold3 = this.pendingHold) === null || _this$pendingHold3 === void 0 || _this$pendingHold3.end();
193
+ }
194
+ connectNow() {
195
+ const connected = Promise.resolve(this);
196
+ this.scheduleFirstDelivery();
197
+ return connected;
198
+ }
199
+ holdNow(attempt, reason, timeout) {
200
+ return new Promise((resolve, reject) => {
201
+ const timer = setTimeout(() => {
202
+ var _this$pendingHold4;
203
+ return (_this$pendingHold4 = this.pendingHold) === null || _this$pendingHold4 === void 0 ? void 0 : _this$pendingHold4.end();
204
+ }, timeout);
205
+ const release = () => {
206
+ clearTimeout(timer);
207
+ this.pendingHold = void 0;
208
+ };
209
+ this.pendingHold = {
210
+ attempt,
211
+ end: () => {
212
+ release();
213
+ resolve(this);
214
+ },
215
+ complete: () => {
216
+ release();
217
+ resolve(this);
218
+ this.scheduleFirstDelivery();
219
+ },
220
+ fail: () => {
221
+ release();
222
+ const error = this.fatalError(reason);
223
+ this.emitter.emit("connectionError", error);
224
+ reject(error);
225
+ }
226
+ };
227
+ });
228
+ }
229
+ failNow(reason) {
230
+ const error = this.fatalError(reason);
231
+ this.emitter.emit("connectionError", error);
232
+ return Promise.reject(error);
233
+ }
234
+ fatalError(reason) {
235
+ return new ConfigDirectorConnectionError(`Connection failed with status: 401. Error: the test client failed this ${reason}. This is an unrecoverable error, will not attempt to reconnect.`, 401);
236
+ }
237
+ scheduleFirstDelivery() {
238
+ const generation = this.attemptGeneration;
239
+ Promise.resolve().then(() => {}).then(() => {
240
+ if (generation !== this.attemptGeneration) return;
241
+ this.connected = true;
242
+ this.deliver(this.fullUpdate());
243
+ });
244
+ }
245
+ deliverIfConnected(configSet) {
246
+ if (this.connected) this.deliver(configSet);
247
+ }
248
+ deliver(configSet) {
249
+ this.pendingDeliveries.push(configSet);
250
+ if (this.delivering) return;
251
+ this.delivering = true;
252
+ try {
253
+ let next = this.pendingDeliveries.shift();
254
+ while (next) {
255
+ this.emitCaught(next);
256
+ next = this.pendingDeliveries.shift();
257
+ }
258
+ } finally {
259
+ this.delivering = false;
260
+ }
261
+ }
262
+ emitCaught(configSet) {
263
+ try {
264
+ this.emitter.emit("configSetReceived", configSet);
265
+ } catch (error) {
266
+ this.logger.error("[InMemoryConnection] Error dispatching a config update from the test client: ", error);
267
+ }
268
+ }
269
+ fullUpdate() {
270
+ return {
271
+ ...TEST_ENVIRONMENT,
272
+ kind: "full",
273
+ configs: Object.fromEntries(this.configs)
274
+ };
275
+ }
276
+ store(values) {
277
+ this.encodeAll(values).forEach((configState, key) => this.configs.set(key, configState));
278
+ }
279
+ encodeAll(values) {
280
+ const encoded = /* @__PURE__ */ new Map();
281
+ for (const [key, value] of Object.entries(values)) encoded.set(key, encodeTestValue(key, value, ++this.revision));
282
+ return encoded;
283
+ }
284
+ };
285
+ //#endregion
286
+ //#region ../js-client-core/src/testing/TestClient.ts
287
+ const TEST_CLIENT_SDK_KEY = "test-client";
288
+ const noopTelemetryClient = {
289
+ updateContext: async () => {},
290
+ evaluatedConfig: () => {},
291
+ close: async () => {}
292
+ };
293
+ var InMemoryTestClient = class {
294
+ constructor(client, connection) {
295
+ this.client = client;
296
+ this.connection = connection;
297
+ }
298
+ setValue(key, value) {
299
+ this.connection.setValue(key, value);
300
+ }
301
+ removeValue(key) {
302
+ this.connection.removeValue(key);
303
+ }
304
+ replaceValues(values) {
305
+ this.connection.replaceValues(values);
306
+ }
307
+ holdInitialization() {
308
+ this.connection.hold("initialize");
309
+ }
310
+ completeInitialization() {
311
+ this.connection.complete("initialize");
312
+ }
313
+ failInitialization() {
314
+ this.connection.fail("initialize");
315
+ }
316
+ holdContextUpdate() {
317
+ this.connection.hold("updateContext");
318
+ }
319
+ completeContextUpdate() {
320
+ this.connection.complete("updateContext");
321
+ }
322
+ failContextUpdate() {
323
+ this.connection.fail("updateContext");
324
+ }
325
+ get contextUpdates() {
326
+ return this.connection.contextUpdates;
327
+ }
328
+ };
329
+ const createTestClient$1 = (sdkOptions, options) => {
330
+ var _options$logger, _options$values;
331
+ const logger = (_options$logger = options === null || options === void 0 ? void 0 : options.logger) !== null && _options$logger !== void 0 ? _options$logger : createDefaultLogger();
332
+ const connection = new InMemoryConnection(logger, (_options$values = options === null || options === void 0 ? void 0 : options.values) !== null && _options$values !== void 0 ? _options$values : {});
333
+ return new InMemoryTestClient(new DefaultConfigDirectorClient(noopTelemetryClient, TEST_CLIENT_SDK_KEY, sdkOptions, {
334
+ logger,
335
+ connection: { timeout: options === null || options === void 0 ? void 0 : options.timeout }
336
+ }, { transport: connection }), connection);
337
+ };
338
+ //#endregion
339
+ //#region ../js-client-core/src/testing/installTestClient.ts
340
+ /**
341
+ * Makes every `ConfigDirectorProvider` mounted without a `client` prop, and every client
342
+ * `withProvider` would build, use `testClient.client` instead of building a client, until the
343
+ * returned function uninstalls it. Construction props such as `sdkKey` are ignored while installed;
344
+ * `context` and `hooks` apply to the installed client as they do for a `client` prop. A provider
345
+ * reads the installed client when it is constructed, so install before rendering.
346
+ *
347
+ * Installing again replaces the installed test client. Each uninstall function restores the test
348
+ * client that was installed when it was created, so nested installs are uninstalled in reverse order.
349
+ *
350
+ * @param testClient The test client whose `client` providers should use
351
+ * @returns A function that uninstalls it
352
+ *
353
+ * @example
354
+ * let uninstallTestClient: () => void;
355
+ * beforeEach(() => {
356
+ * testClient = createTestClient({ values: { "new-checkout": true } });
357
+ * uninstallTestClient = installTestClient(testClient);
358
+ * });
359
+ * afterEach(() => uninstallTestClient());
360
+ */
361
+ const installTestClient = (testClient) => {
362
+ const slot = globalThis;
363
+ const previous = slot[INSTALLED_TEST_CLIENT_SLOT];
364
+ slot[INSTALLED_TEST_CLIENT_SLOT] = testClient.client;
365
+ return () => {
366
+ slot[INSTALLED_TEST_CLIENT_SLOT] = previous;
367
+ };
368
+ };
369
+ //#endregion
370
+ //#region src/testing.ts
371
+ /**
372
+ * Creates a {@link TestClient}: a real `ConfigDirectorClient` connected to an in-memory server
373
+ * that the test controls. No network connection is opened and no telemetry is sent. Render with
374
+ * `<ConfigDirectorProvider client={testClient.client}>`, or install it with `installTestClient` for
375
+ * components that render their own provider.
376
+ *
377
+ * @param options {@link TestClientOptions}: the initial values, the client's timeout, and its logger
378
+ * @returns A {@link TestClient} whose `client` is passed to the code under test
379
+ *
380
+ * @example
381
+ * import { createTestClient } from "@configdirector/react-native-sdk/testing";
382
+ *
383
+ * const testClient = createTestClient({ values: { "new-checkout": true } });
384
+ * const view = render(
385
+ * <ConfigDirectorProvider client={testClient.client}>
386
+ * <Home />
387
+ * </ConfigDirectorProvider>,
388
+ * );
389
+ * act(() => testClient.setValue("new-checkout", false));
390
+ */
391
+ const createTestClient = (options) => {
392
+ return createTestClient$1({
393
+ sdkName: "react-native-sdk",
394
+ sdkVersion: "1.6.0"
395
+ }, options);
396
+ };
397
+ //#endregion
398
+ export { ConfigDirectorConnectionError, ConfigDirectorValidationError, createTestClient, installTestClient };