@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 +43 -0
- package/dist/configdirector-react-native-testing.d.mts +157 -0
- package/dist/configdirector-react-native-testing.mjs +398 -0
- package/dist/configdirector-react-native.d.mts +48 -294
- package/dist/configdirector-react-native.mjs +224 -1335
- package/dist/installedTestClient-CxGEAMOE.mjs +1341 -0
- package/dist/types-fcPMBgiX.d.mts +295 -0
- package/package.json +14 -1
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 };
|