Skip to content

Commit d0d196d

Browse files
authored
fix: add additional client tests and fixes (#8)
1 parent 2340428 commit d0d196d

File tree

2 files changed

+190
-23
lines changed

2 files changed

+190
-23
lines changed

packages/browser/src/experimentClient.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,12 @@ export class ExperimentClient implements Client {
147147
if (!this.user) {
148148
return this.user;
149149
}
150-
const userPropertiesCopy = { ...this.user.user_properties };
151-
return { ...this.user, user_properties: userPropertiesCopy };
150+
if (this.user?.user_properties) {
151+
const userPropertiesCopy = { ...this.user.user_properties };
152+
return { ...this.user, user_properties: userPropertiesCopy };
153+
} else {
154+
return { ...this.user };
155+
}
152156
}
153157

154158
/**
@@ -161,8 +165,12 @@ export class ExperimentClient implements Client {
161165
this.user = null;
162166
return;
163167
}
164-
const userPropertiesCopy = { ...user.user_properties };
165-
this.user = { ...user, user_properties: userPropertiesCopy };
168+
if (this.user?.user_properties) {
169+
const userPropertiesCopy = { ...user.user_properties };
170+
this.user = { ...user, user_properties: userPropertiesCopy };
171+
} else {
172+
this.user = { ...user };
173+
}
166174
}
167175

168176
/**
@@ -318,15 +326,15 @@ export class ExperimentClient implements Client {
318326
private sourceVariants(): Variants {
319327
if (this.config.source == Source.LocalStorage) {
320328
return this.storage.getAll();
321-
} else if (this.config == Source.InitialVariants) {
329+
} else if (this.config.source == Source.InitialVariants) {
322330
return this.config.initialVariants;
323331
}
324332
}
325333

326334
private secondaryVariants(): Variants {
327335
if (this.config.source == Source.LocalStorage) {
328336
return this.config.initialVariants;
329-
} else if (this.config == Source.InitialVariants) {
337+
} else if (this.config.source == Source.InitialVariants) {
330338
return this.storage.getAll();
331339
}
332340
}
Lines changed: 176 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,197 @@
1+
import { Source } from '../src/config';
12
import { ExperimentClient } from '../src/experimentClient';
2-
import { ExperimentUser } from '../src/types/user';
3+
import { ExperimentUser, ExperimentUserProvider } from '../src/types/user';
4+
import { Variant, Variants } from '../src/types/variant';
5+
6+
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
7+
8+
class TestUserProvider implements ExperimentUserProvider {
9+
getUser(): ExperimentUser {
10+
return testUser;
11+
}
12+
}
313

414
const API_KEY = 'client-DvWljIjiiuqLbyjqdvBaLFfEBrAvGuA3';
515

616
const testUser: ExperimentUser = { user_id: 'test_user' };
7-
const testClient: ExperimentClient = new ExperimentClient(API_KEY, {});
817

9-
const testTimeoutNoRetriesClient = new ExperimentClient(API_KEY, {
10-
retryFetchOnFailure: false,
11-
fetchTimeoutMillis: 1,
12-
});
18+
const serverKey = 'sdk-ci-test';
19+
const serverVariant: Variant = { value: 'on', payload: 'payload' };
20+
const serverOffVariant: Variant = { value: 'off' };
1321

14-
const testTimeoutRetrySuccessClient = new ExperimentClient(API_KEY, {
15-
fetchTimeoutMillis: 1,
16-
});
22+
const initialKey = 'initial-key';
23+
const initialVariant: Variant = { value: 'initial' };
24+
25+
const initialVariants: Variants = {
26+
'sdk-ci-test': serverOffVariant,
27+
'initial-key': initialVariant,
28+
};
29+
30+
const fallbackVariant: Variant = { value: 'fallback', payload: 'payload' };
31+
const explicitFallbackString = 'first';
32+
const explicitFallbackVariant: Variant = { value: explicitFallbackString };
33+
const unknownKey = 'not-a-valid-key';
1734

1835
beforeEach(() => {
1936
localStorage.clear();
2037
});
2138

39+
/**
40+
* Basic test that fetching variants for a user succeeds.
41+
*/
2242
test('ExperimentClient.fetch, success', async () => {
23-
await testClient.fetch(testUser);
24-
const variant = testClient.variant('sdk-ci-test');
25-
expect(variant).toEqual({ value: 'on', payload: 'payload' });
43+
const client = new ExperimentClient(API_KEY, {});
44+
await client.fetch(testUser);
45+
const variant = client.variant(serverKey);
46+
expect(variant).toEqual(serverVariant);
2647
});
2748

49+
/**
50+
* Test that a timed out fetch request with retries disabled does not fetch any
51+
* variants.
52+
*/
2853
test('ExperimentClient.fetch, no retries, timeout failure', async () => {
29-
await testTimeoutNoRetriesClient.fetch(testUser);
30-
const variants = testTimeoutNoRetriesClient.all();
54+
const client = new ExperimentClient(API_KEY, {
55+
retryFetchOnFailure: false,
56+
fetchTimeoutMillis: 1,
57+
});
58+
await client.fetch(testUser);
59+
const variants = client.all();
3160
expect(variants).toEqual({});
3261
});
3362

34-
test('ExperimentClient.fetch, no retries, timeout failure', async () => {
35-
await testTimeoutRetrySuccessClient.fetch(testUser);
36-
const variant = testClient.variant('sdk-ci-test');
63+
/**
64+
* Test that a timed out fetch request with (background) retries enabled will
65+
* complete successfully within a reasonable amount of time.
66+
*/
67+
test('ExperimentClient.fetch, with retries, retry success', async () => {
68+
const client = new ExperimentClient(API_KEY, {
69+
fallbackVariant: fallbackVariant,
70+
fetchTimeoutMillis: 1,
71+
});
72+
await client.fetch(testUser);
73+
let variant = client.variant(serverKey);
74+
expect(variant).toEqual(fallbackVariant);
75+
await delay(2000);
76+
variant = client.variant(serverKey);
77+
expect(variant).toEqual(serverVariant);
78+
});
79+
80+
/**
81+
* Test that the client always prefers the explicit fallback over any
82+
* configured fallbacks when there are no stored variants--even if the
83+
* provided key is present in the initialVariants config.
84+
*/
85+
test('ExperimentClient.variant, no stored variants, explicit fallback returned', () => {
86+
let variant: Variant;
87+
const client = new ExperimentClient(API_KEY, {
88+
fallbackVariant: fallbackVariant,
89+
initialVariants: initialVariants,
90+
});
91+
92+
variant = client.variant(unknownKey, explicitFallbackVariant);
93+
expect(variant).toEqual(explicitFallbackVariant);
94+
95+
variant = client.variant(unknownKey, explicitFallbackString);
96+
expect(variant).toEqual(explicitFallbackVariant);
97+
98+
variant = client.variant(initialKey, explicitFallbackVariant);
99+
expect(variant).toEqual(explicitFallbackVariant);
100+
101+
variant = client.variant(initialKey, explicitFallbackString);
102+
expect(variant).toEqual(explicitFallbackVariant);
103+
});
104+
105+
/**
106+
* Test that the client falls back to the configured `fallbackVariant` for an
107+
* unknown key when no explicit fallback is provided.
108+
*/
109+
test('ExperimentClient.variant, unknown key returns default fallback', () => {
110+
const client = new ExperimentClient(API_KEY, {
111+
fallbackVariant: fallbackVariant,
112+
initialVariants: initialVariants,
113+
});
114+
const variant: Variant = client.variant(unknownKey);
115+
expect(variant).toEqual(fallbackVariant);
116+
});
117+
118+
/**
119+
* Test that the client falls back to the configured `initialVariants` for
120+
* flag keys included in the initial set. After a fetch, the client should
121+
* take flags from local storage instead.
122+
*/
123+
test('ExperimentClient.variant, initial variants fallback before fetch, no fallback after fetch', async () => {
124+
let variant: Variant;
125+
const client = new ExperimentClient(API_KEY, {
126+
fallbackVariant: fallbackVariant,
127+
initialVariants: initialVariants,
128+
});
129+
130+
variant = client.variant(initialKey);
131+
expect(variant).toEqual(initialVariant);
132+
133+
variant = client.variant(serverKey);
134+
expect(variant).toEqual(serverOffVariant);
135+
136+
await client.fetch();
137+
138+
variant = client.variant(initialKey);
139+
expect(variant).toEqual(initialVariant);
140+
141+
variant = client.variant(serverKey);
142+
expect(variant).toEqual(serverVariant);
143+
});
144+
145+
/**
146+
* Calling `all()` prior to fetch with an empty storage returns configured
147+
* initial variants.
148+
*/
149+
test('ExperimentClient.all, initial variants returned', async () => {
150+
const client = new ExperimentClient(API_KEY, {
151+
initialVariants: initialVariants,
152+
});
153+
const variants = client.all();
154+
expect(variants).toEqual(initialVariants);
155+
});
156+
157+
/**
158+
* Setting source to initial variants will prioritize variants in initial
159+
* variants over those stored in local storage.
160+
*/
161+
test('ExperimentClient.fetch, initial variants source, prefer initial', async () => {
162+
const client = new ExperimentClient(API_KEY, {
163+
source: Source.InitialVariants,
164+
initialVariants: initialVariants,
165+
});
166+
let variant = client.variant(serverKey);
167+
expect(variant).toEqual(serverOffVariant);
168+
await client.fetch(testUser);
169+
variant = client.variant(serverKey);
170+
expect(variant).toEqual(serverOffVariant);
171+
});
172+
173+
/**
174+
* Test that fetch with an explicit user arguement will set the user within the
175+
* client, and calling setUser() after will overwrite the user.
176+
*/
177+
test('ExperimentClient.fetch, sets user, setUser overrides', async () => {
178+
const client = new ExperimentClient(API_KEY, {});
179+
await client.fetch(testUser);
180+
expect(client.getUser()).toEqual(testUser);
181+
const newUser = { user_id: 'new_test_user' };
182+
client.setUser(newUser);
183+
expect(client.getUser()).toEqual(newUser);
184+
});
185+
186+
/**
187+
* Test that fetch with a user provided by a user provider rather than an
188+
* explicit user argument is successful.
189+
*/
190+
test('ExperimentClient.fetch, with user provider, success', async () => {
191+
const client = new ExperimentClient(API_KEY, {}).setUserProvider(
192+
new TestUserProvider(),
193+
);
194+
await client.fetch();
195+
const variant = client.variant('sdk-ci-test');
37196
expect(variant).toEqual({ value: 'on', payload: 'payload' });
38197
});

0 commit comments

Comments
 (0)