-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathbrowser.spec.js
More file actions
359 lines (320 loc) · 15.9 KB
/
browser.spec.js
File metadata and controls
359 lines (320 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
import tape from 'tape-catch';
import sinon from 'sinon';
import fetchMock from '../testUtils/fetchMock';
import { url } from '../testUtils';
import { SplitFactory } from '../../';
import { settingsFactory } from '../../settings';
const settings = settingsFactory({ core: { key: 'facundo@split.io' } });
const spySplitChanges = sinon.spy();
const spySegmentChanges = sinon.spy();
const spyMemberships = sinon.spy();
const spyEventsBulk = sinon.spy();
const spyTestImpressionsBulk = sinon.spy();
const spyTestImpressionsCount = sinon.spy();
const spyMetricsTimes = sinon.spy();
const spyMetricsCounters = sinon.spy();
const spyAny = sinon.spy();
// helper function that should call the spy function and return a 200 to keep
// going the fetch request flow
const replySpy = spy => {
spy();
return 200;
};
const configMocks = () => {
fetchMock.mock(new RegExp(`${url(settings, '/splitChanges/')}.*`), () => replySpy(spySplitChanges));
fetchMock.mock(new RegExp(`${url(settings, '/segmentChanges/')}.*`), () => replySpy(spySegmentChanges));
fetchMock.mock(new RegExp(`${url(settings, '/memberships/')}.*`), () => replySpy(spyMemberships));
fetchMock.mock(url(settings, '/events/bulk'), () => replySpy(spyEventsBulk));
fetchMock.mock(url(settings, '/testImpressions/bulk'), () => replySpy(spyTestImpressionsBulk));
fetchMock.mock(url(settings, '/testImpressions/count'), () => replySpy(spyTestImpressionsCount));
fetchMock.mock('*', () => replySpy(spyAny));
};
tape('Browser offline mode', function (assert) {
configMocks();
const originalFeaturesMap = {
testing_split: 'on',
testing_split_with_config: {
treatment: 'off',
config: '{ "color": "blue" }',
sets: []
}
};
const config = {
core: {
// Although `key` is mandatory according to TypeScript declaration files,
// it can be omitted in LOCALHOST mode. In that case, the value `localhost_key` is used.
authorizationKey: 'localhost'
},
scheduler: {
impressionsRefreshRate: 0.01,
eventsPushRate: 0.01,
offlineRefreshRate: 0.19
},
startup: {
eventsFirstPushWindow: 0
},
features: originalFeaturesMap
};
const factory = SplitFactory(config);
const manager = factory.manager();
const client = factory.client();
const sharedClient = factory.client('nicolas.zelaya@split.io');
// Tracking some events to test they are not flushed.
assert.true(client.track('a_tt', 'an_ev_id'));
assert.true(client.track('another_tt', 'another_ev_id', 25));
assert.false(client.track({}, [], 'invalid_stuff'));
assert.true(sharedClient.track('a_tt', 'another_ev_id', 10));
assert.equal(client.getTreatment('testing_split'), 'control', 'control due to not ready');
assert.equal(sharedClient.getTreatment('testing_split'), 'control', 'control due to not ready');
assert.equal(manager.splits().length, 0);
// SDK events on shared client
let sharedReadyCount = 0;
sharedClient.on(sharedClient.Event.SDK_READY, function () {
assert.equal(sharedClient.getTreatment('testing_split'), 'on');
sharedReadyCount++;
});
let sharedUpdateCount = 0;
sharedClient.on(sharedClient.Event.SDK_UPDATE, function () {
sharedUpdateCount++;
});
const configs = [
{ ...config, features: { ...config.features }, storage: { type: 'INVALID TYPE' } },
{ ...config, storage: { type: 'LOCALSTORAGE' } },
{ ...config },
config,
];
const factories = configs.map(config => SplitFactory(config));
let readyCount = 0, updateCount = 0, readyFromCacheCount = 0;
for (let i = 0; i < factories.length; i++) {
const factory = factories[i], client = factory.client(), manager = factory.manager(), client2 = factory.client('other');
client.on(client.Event.SDK_READY, () => {
assert.deepEqual(manager.names(), ['testing_split', 'testing_split_with_config']);
assert.equal(client.getTreatment('testing_split_with_config'), 'off');
readyCount++;
});
client.on(client.Event.SDK_UPDATE, () => {
assert.deepEqual(manager.names().sort(), ['testing_split', 'testing_split_2', 'testing_split_3', 'testing_split_with_config']);
assert.equal(client.getTreatment('testing_split_with_config'), 'nope');
updateCount++;
});
const sdkReadyFromCache = (client) => () => {
const clientStatus = client.__getStatus();
assert.equal(clientStatus.isReadyFromCache, true, 'If ready from cache, READY_FROM_CACHE status must be true');
assert.equal(clientStatus.isReady, false, 'READY status must not be set before READY_FROM_CACHE');
assert.deepEqual(manager.names(), ['testing_split', 'testing_split_with_config']);
assert.equal(client.getTreatment('testing_split_with_config'), 'off');
readyFromCacheCount++;
client.on(client.Event.SDK_READY_FROM_CACHE, () => {
assert.fail('It should not emit SDK_READY_FROM_CACHE again');
});
const newClient = factory.client('another');
assert.equal(newClient.getTreatment('testing_split_with_config'), 'off', 'It should evaluate treatments with data from cache instead of control');
newClient.on(newClient.Event.SDK_READY_FROM_CACHE, () => {
assert.fail('It should not emit SDK_READY_FROM_CACHE if already done.');
});
};
client.on(client.Event.SDK_READY_FROM_CACHE, sdkReadyFromCache(client));
client2.on(client2.Event.SDK_READY_FROM_CACHE, sdkReadyFromCache(client2));
}
client.once(client.Event.SDK_READY, function () {
const readyTimestamp = Date.now();
// Check the information through the client original instance
assert.equal(client.getTreatment('testing_split'), 'on');
assert.equal(client.getTreatment('testing_split_2'), 'control');
assert.equal(client.getTreatment('testing_split_with_config'), 'off');
assert.deepEqual(client.getTreatments([
'testing_split',
'testing_split_2',
'testing_split_with_config'
]), {
testing_split: 'on',
testing_split_2: 'control',
testing_split_with_config: 'off'
});
// with config
assert.deepEqual(client.getTreatmentWithConfig('testing_split'), { treatment: 'on', config: null });
assert.deepEqual(client.getTreatmentWithConfig('testing_split_with_config'), { treatment: 'off', config: '{ "color": "blue" }' });
assert.deepEqual(client.getTreatmentsWithConfig([
'testing_split',
'testing_split_2',
'testing_split_with_config'
]), {
testing_split: { treatment: 'on', config: null },
testing_split_2: { treatment: 'control', config: null },
testing_split_with_config: { treatment: 'off', config: '{ "color": "blue" }' }
});
// Manager tests
const expectedSplitView1 = {
name: 'testing_split', trafficType: 'localhost', killed: false, changeNumber: 0, treatments: ['on'], configs: {}, defaultTreatment: 'control', sets: []
};
const expectedSplitView2 = {
name: 'testing_split_with_config', trafficType: 'localhost', killed: false, changeNumber: 0, treatments: ['off'], configs: { off: '{ "color": "blue" }' }, defaultTreatment: 'control', sets: []
};
assert.deepEqual(manager.names(), ['testing_split', 'testing_split_with_config']);
assert.deepEqual(manager.split('testing_split'), expectedSplitView1);
assert.deepEqual(manager.split('testing_split_with_config'), expectedSplitView2);
assert.deepEqual(manager.split('not_existent'), null);
assert.deepEqual(manager.splits(), [expectedSplitView1, expectedSplitView2]);
const otherSharedClient = factory.client('emiliano.sanchez@split.io');
assert.equal(otherSharedClient.getTreatment('testing_split'), 'control');
otherSharedClient.on(otherSharedClient.Event.SDK_READY, function () {
assert.equal(otherSharedClient.getTreatment('testing_split'), 'on');
});
// And then through the shared instance.
// We use ready promise since SDK_READY may have been emitted for the shared client (not in this case anyway)
otherSharedClient.ready().then(() => {
assert.equal(otherSharedClient.getTreatment('testing_split'), 'on');
assert.equal(otherSharedClient.getTreatment('testing_split_2'), 'control');
assert.deepEqual(otherSharedClient.getTreatments([
'testing_split',
'testing_split_2',
'testing_split_with_config'
]), {
testing_split: 'on',
testing_split_2: 'control',
testing_split_with_config: 'off'
});
// with config
assert.deepEqual(otherSharedClient.getTreatmentWithConfig('testing_split'), { treatment: 'on', config: null });
assert.deepEqual(otherSharedClient.getTreatmentsWithConfig([
'testing_split',
'testing_split_2',
'testing_split_with_config'
]), {
testing_split: { treatment: 'on', config: null },
testing_split_2: { treatment: 'control', config: null },
testing_split_with_config: { treatment: 'off', config: '{ "color": "blue" }' }
});
});
setTimeout(() => {
// Update features reference in settings
factory.settings.features = {
testing_split: 'on',
testing_split_2: 'off',
testing_split_3: 'custom_treatment',
testing_split_with_config: {
treatment: 'nope',
config: null
}
};
// Update features properties in config
configs[0].features['testing_split'] = 'on';
configs[0].features['testing_split_2'] = 'off';
configs[0].features['testing_split_3'] = 'custom_treatment';
configs[0].features['testing_split_with_config'] = {
treatment: 'nope',
config: null
};
// Update the features in all remaining factories except the last one
for (let i = 1; i < factories.length - 1; i++) {
factories[i].settings.features = factory.settings.features;
}
// Assigning a new object to the features property in the config doesn't trigger an update
configs[configs.length - 1].features = { ...factory.settings.features };
}, 1000);
setTimeout(() => { factory.settings.features = originalFeaturesMap; }, 200);
setTimeout(() => { factory.settings.features = { testing_split: 'on', testing_split_with_config: { treatment: 'off', config: '{ "color": "blue" }' } }; }, 400);
setTimeout(() => { factory.settings.features = originalFeaturesMap; }, 600);
setTimeout(() => { factory.settings.features = { testing_split: 'on', testing_split_with_config: { treatment: 'off', config: '{ "color": "blue" }' } }; }, 750);
// once updated, test again.
client.once(client.Event.SDK_UPDATE, function () {
assert.true((Date.now() - readyTimestamp) > 1000, 'Should only emit SDK_UPDATE after a real update.');
client.once(client.Event.SDK_UPDATE, function () { assert.fail('Should not emit a second SDK_UPDATE event'); });
assert.equal(client.getTreatment('testing_split_2'), 'off');
assert.equal(client.getTreatment('testing_split_3'), 'custom_treatment');
assert.deepEqual(client.getTreatmentWithConfig('testing_split_3'), { treatment: 'custom_treatment', config: null });
assert.deepEqual(client.getTreatmentWithConfig('testing_split_with_config'), { treatment: 'nope', config: null });
assert.deepEqual(client.getTreatments([
'testing_split',
'testing_split_2',
'testing_split_3',
'testing_split_with_config',
'testing_not_exist'
]), {
testing_split: 'on',
testing_split_2: 'off',
testing_split_3: 'custom_treatment',
testing_split_with_config: 'nope',
testing_not_exist: 'control'
});
assert.deepEqual(client.getTreatmentsWithConfig([
'testing_split_2',
'testing_split_3',
'testing_split_with_config'
]), {
testing_split_2: { treatment: 'off', config: null },
testing_split_3: { treatment: 'custom_treatment', config: null },
testing_split_with_config: { treatment: 'nope', config: null }
});
// Manager tests
const expectedSplitView3 = {
name: 'testing_split_with_config', trafficType: 'localhost', killed: false, changeNumber: 0, treatments: ['nope'], configs: {}, defaultTreatment: 'control', sets: []
};
assert.deepEqual(manager.names(), ['testing_split', 'testing_split_2', 'testing_split_3', 'testing_split_with_config']);
assert.deepEqual(manager.split('testing_split'), expectedSplitView1);
assert.deepEqual(manager.split('not_existent'), null);
assert.deepEqual(manager.split('testing_split_with_config'), expectedSplitView3);
assert.deepEqual(manager.splits(), [
expectedSplitView1,
{
...expectedSplitView3, name: 'testing_split_2', treatments: ['off']
},
{
...expectedSplitView3, name: 'testing_split_3', treatments: ['custom_treatment']
},
expectedSplitView3
]);
// Test shared client for the same data
assert.equal(sharedClient.getTreatment('testing_split_2'), 'off');
assert.equal(sharedClient.getTreatment('testing_split_3'), 'custom_treatment');
assert.deepEqual(sharedClient.getTreatmentWithConfig('testing_split_3'), { treatment: 'custom_treatment', config: null });
assert.deepEqual(sharedClient.getTreatmentWithConfig('testing_split_with_config'), { treatment: 'nope', config: null });
assert.deepEqual(sharedClient.getTreatments([
'testing_split',
'testing_split_2',
'testing_split_3',
'testing_not_exist'
]), {
testing_split: 'on',
testing_split_2: 'off',
testing_split_3: 'custom_treatment',
testing_not_exist: 'control'
});
assert.deepEqual(sharedClient.getTreatmentsWithConfig([
'testing_split_3',
'testing_not_exist'
]), {
testing_split_3: { treatment: 'custom_treatment', config: null },
testing_not_exist: { treatment: 'control', config: null }
});
// timeout to wait SDK_UPDATE on all factories
setTimeout(() => {
const destroyPromises = [
sharedClient.destroy(), client.destroy(),
...factories.map(f => f.client().destroy())
];
// When both promises have been resolved, we check for network activity
Promise.all(destroyPromises).then(() => {
// We test the breakdown instead of just the misc because it's faster to spot where the issue is
assert.notOk(spySplitChanges.called, 'On offline mode we should not call the splitChanges endpoint.');
assert.notOk(spySegmentChanges.called, 'On offline mode we should not call the segmentChanges endpoint.');
assert.notOk(spyMemberships.called, 'On offline mode we should not call the Memberships endpoint.');
assert.notOk(spyEventsBulk.called, 'On offline mode we should not call the events endpoint.');
assert.notOk(spyTestImpressionsBulk.called, 'On offline mode we should not call the impressions endpoint.');
assert.notOk(spyTestImpressionsCount.called, 'On offline mode we should not call the impressions count endpoint.');
assert.notOk(spyMetricsTimes.called, 'On offline mode we should not call the metric times endpoint.');
assert.notOk(spyMetricsCounters.called, 'On offline mode we should not call the metric counters endpoint.');
assert.notOk(spyAny.called, 'On offline mode we should NOT call to ANY endpoint, we are completely isolated from BE.');
// SDK events on shared client
assert.equal(sharedReadyCount, 1, 'Shared client should have emitted SDK_READY event once');
assert.equal(sharedUpdateCount, 1, 'Shared client should have emitted SDK_UPDATE event once');
// SDK events on other factory clients
assert.equal(readyCount, factories.length, 'Each factory client should have emitted SDK_READY event once');
assert.equal(updateCount, factories.length - 1, 'Each factory client except one should have emitted SDK_UPDATE event once');
assert.equal(readyFromCacheCount, 2, 'The main and shared client of the factory with LOCALSTORAGE should have emitted SDK_READY_FROM_CACHE event');
assert.end();
});
});
}, 3500);
});
});