swetrix 3.7.2 → 4.0.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.
@@ -0,0 +1,330 @@
1
+ import { init, getExperiment, getExperiments, clearExperimentsCache } from '../src/index'
2
+ import { Lib } from '../src/Lib'
3
+
4
+ // Mock fetch globally
5
+ const mockFetch = jest.fn()
6
+ global.fetch = mockFetch
7
+
8
+ describe('A/B Testing Experiments', () => {
9
+ const PROJECT_ID = 'test-project-id'
10
+ let libInstance: Lib
11
+
12
+ beforeEach(() => {
13
+ jest.clearAllMocks()
14
+ jest.resetModules()
15
+
16
+ // Reset fetch mock
17
+ mockFetch.mockReset()
18
+
19
+ Object.defineProperty(window, 'location', {
20
+ value: {
21
+ hostname: 'example.com',
22
+ pathname: '/test-page',
23
+ hash: '',
24
+ search: '',
25
+ },
26
+ writable: true,
27
+ })
28
+ })
29
+
30
+ describe('getExperiments', () => {
31
+ beforeEach(async () => {
32
+ jest.resetModules()
33
+ const { init: freshInit } = await import('../src/index')
34
+ libInstance = freshInit(PROJECT_ID, { devMode: true }) as Lib
35
+ })
36
+
37
+ test('should return experiments from API response', async () => {
38
+ // Arrange
39
+ mockFetch.mockResolvedValueOnce({
40
+ ok: true,
41
+ json: async () => ({
42
+ flags: { 'feature-flag-1': true },
43
+ experiments: {
44
+ 'exp-checkout': 'variant-b',
45
+ 'exp-pricing': 'control',
46
+ },
47
+ }),
48
+ })
49
+
50
+ // Act
51
+ const { getExperiments: freshGetExperiments } = await import('../src/index')
52
+ const experiments = await freshGetExperiments()
53
+
54
+ // Assert
55
+ expect(experiments).toEqual({
56
+ 'exp-checkout': 'variant-b',
57
+ 'exp-pricing': 'control',
58
+ })
59
+ })
60
+
61
+ test('should return empty object when no experiments in response', async () => {
62
+ // Arrange
63
+ mockFetch.mockResolvedValueOnce({
64
+ ok: true,
65
+ json: async () => ({
66
+ flags: { 'feature-flag-1': true },
67
+ }),
68
+ })
69
+
70
+ // Act
71
+ const { getExperiments: freshGetExperiments } = await import('../src/index')
72
+ const experiments = await freshGetExperiments()
73
+
74
+ // Assert
75
+ expect(experiments).toEqual({})
76
+ })
77
+
78
+ test('should use cached experiments on subsequent calls', async () => {
79
+ // Arrange
80
+ mockFetch.mockResolvedValueOnce({
81
+ ok: true,
82
+ json: async () => ({
83
+ flags: {},
84
+ experiments: { 'exp-1': 'variant-a' },
85
+ }),
86
+ })
87
+
88
+ // Act
89
+ const { getExperiments: freshGetExperiments } = await import('../src/index')
90
+ await freshGetExperiments()
91
+ await freshGetExperiments()
92
+
93
+ // Assert - fetch should only be called once
94
+ expect(mockFetch).toHaveBeenCalledTimes(1)
95
+ })
96
+
97
+ test('should bypass cache when forceRefresh is true', async () => {
98
+ // Arrange
99
+ mockFetch
100
+ .mockResolvedValueOnce({
101
+ ok: true,
102
+ json: async () => ({
103
+ flags: {},
104
+ experiments: { 'exp-1': 'variant-a' },
105
+ }),
106
+ })
107
+ .mockResolvedValueOnce({
108
+ ok: true,
109
+ json: async () => ({
110
+ flags: {},
111
+ experiments: { 'exp-1': 'variant-b' },
112
+ }),
113
+ })
114
+
115
+ // Act
116
+ const { getExperiments: freshGetExperiments } = await import('../src/index')
117
+ const first = await freshGetExperiments()
118
+ const second = await freshGetExperiments(undefined, true)
119
+
120
+ // Assert
121
+ expect(mockFetch).toHaveBeenCalledTimes(2)
122
+ expect(first).toEqual({ 'exp-1': 'variant-a' })
123
+ expect(second).toEqual({ 'exp-1': 'variant-b' })
124
+ })
125
+ })
126
+
127
+ describe('getExperiment', () => {
128
+ beforeEach(async () => {
129
+ jest.resetModules()
130
+ const { init: freshInit } = await import('../src/index')
131
+ libInstance = freshInit(PROJECT_ID, { devMode: true }) as Lib
132
+ })
133
+
134
+ test('should return specific experiment variant', async () => {
135
+ // Arrange
136
+ mockFetch.mockResolvedValueOnce({
137
+ ok: true,
138
+ json: async () => ({
139
+ flags: {},
140
+ experiments: {
141
+ 'exp-checkout': 'new-checkout',
142
+ 'exp-pricing': 'control',
143
+ },
144
+ }),
145
+ })
146
+
147
+ // Act
148
+ const { getExperiment: freshGetExperiment } = await import('../src/index')
149
+ const variant = await freshGetExperiment('exp-checkout')
150
+
151
+ // Assert
152
+ expect(variant).toBe('new-checkout')
153
+ })
154
+
155
+ test('should return defaultVariant when experiment not found', async () => {
156
+ // Arrange
157
+ mockFetch.mockResolvedValueOnce({
158
+ ok: true,
159
+ json: async () => ({
160
+ flags: {},
161
+ experiments: {},
162
+ }),
163
+ })
164
+
165
+ // Act
166
+ const { getExperiment: freshGetExperiment } = await import('../src/index')
167
+ const variant = await freshGetExperiment('non-existent', undefined, 'fallback')
168
+
169
+ // Assert
170
+ expect(variant).toBe('fallback')
171
+ })
172
+
173
+ test('should return null when experiment not found and no default provided', async () => {
174
+ // Arrange
175
+ mockFetch.mockResolvedValueOnce({
176
+ ok: true,
177
+ json: async () => ({
178
+ flags: {},
179
+ experiments: {},
180
+ }),
181
+ })
182
+
183
+ // Act
184
+ const { getExperiment: freshGetExperiment } = await import('../src/index')
185
+ const variant = await freshGetExperiment('non-existent')
186
+
187
+ // Assert
188
+ expect(variant).toBeNull()
189
+ })
190
+ })
191
+
192
+ describe('clearExperimentsCache', () => {
193
+ test('should clear cache and fetch fresh data on next call', async () => {
194
+ jest.resetModules()
195
+ const {
196
+ init: freshInit,
197
+ getExperiments: freshGetExperiments,
198
+ clearExperimentsCache: freshClearCache,
199
+ } = await import('../src/index')
200
+ freshInit(PROJECT_ID, { devMode: true })
201
+
202
+ // Arrange
203
+ mockFetch
204
+ .mockResolvedValueOnce({
205
+ ok: true,
206
+ json: async () => ({
207
+ flags: {},
208
+ experiments: { 'exp-1': 'variant-a' },
209
+ }),
210
+ })
211
+ .mockResolvedValueOnce({
212
+ ok: true,
213
+ json: async () => ({
214
+ flags: {},
215
+ experiments: { 'exp-1': 'variant-b' },
216
+ }),
217
+ })
218
+
219
+ // Act
220
+ await freshGetExperiments()
221
+ freshClearCache()
222
+ const newExperiments = await freshGetExperiments()
223
+
224
+ // Assert
225
+ expect(mockFetch).toHaveBeenCalledTimes(2)
226
+ expect(newExperiments).toEqual({ 'exp-1': 'variant-b' })
227
+ })
228
+ })
229
+
230
+ describe('profileId handling', () => {
231
+ test('should include profileId in request when provided in options', async () => {
232
+ jest.resetModules()
233
+ const { init: freshInit, getExperiments: freshGetExperiments } = await import('../src/index')
234
+ freshInit(PROJECT_ID, { devMode: true })
235
+
236
+ mockFetch.mockResolvedValueOnce({
237
+ ok: true,
238
+ json: async () => ({
239
+ flags: {},
240
+ experiments: {},
241
+ }),
242
+ })
243
+
244
+ // Act
245
+ await freshGetExperiments({ profileId: 'user-123' })
246
+
247
+ // Assert
248
+ expect(mockFetch).toHaveBeenCalledWith(
249
+ expect.any(String),
250
+ expect.objectContaining({
251
+ body: JSON.stringify({
252
+ pid: PROJECT_ID,
253
+ profileId: 'user-123',
254
+ }),
255
+ }),
256
+ )
257
+ })
258
+
259
+ test('should use global profileId when not provided in options', async () => {
260
+ jest.resetModules()
261
+ const { init: freshInit, getExperiments: freshGetExperiments } = await import('../src/index')
262
+ freshInit(PROJECT_ID, { devMode: true, profileId: 'global-user' })
263
+
264
+ mockFetch.mockResolvedValueOnce({
265
+ ok: true,
266
+ json: async () => ({
267
+ flags: {},
268
+ experiments: {},
269
+ }),
270
+ })
271
+
272
+ // Act
273
+ await freshGetExperiments()
274
+
275
+ // Assert
276
+ expect(mockFetch).toHaveBeenCalledWith(
277
+ expect.any(String),
278
+ expect.objectContaining({
279
+ body: JSON.stringify({
280
+ pid: PROJECT_ID,
281
+ profileId: 'global-user',
282
+ }),
283
+ }),
284
+ )
285
+ })
286
+ })
287
+
288
+ describe('error handling', () => {
289
+ test('should return empty object when fetch fails', async () => {
290
+ jest.resetModules()
291
+ const { init: freshInit, getExperiments: freshGetExperiments } = await import('../src/index')
292
+ freshInit(PROJECT_ID, { devMode: true })
293
+
294
+ // Arrange
295
+ mockFetch.mockRejectedValueOnce(new Error('Network error'))
296
+
297
+ // Suppress console.warn for this test
298
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
299
+
300
+ // Act
301
+ const experiments = await freshGetExperiments()
302
+
303
+ // Assert
304
+ expect(experiments).toEqual({})
305
+ consoleSpy.mockRestore()
306
+ })
307
+
308
+ test('should return empty object when response is not ok', async () => {
309
+ jest.resetModules()
310
+ const { init: freshInit, getExperiments: freshGetExperiments } = await import('../src/index')
311
+ freshInit(PROJECT_ID, { devMode: true })
312
+
313
+ // Arrange
314
+ mockFetch.mockResolvedValueOnce({
315
+ ok: false,
316
+ status: 500,
317
+ })
318
+
319
+ // Suppress console.warn for this test
320
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
321
+
322
+ // Act
323
+ const experiments = await freshGetExperiments()
324
+
325
+ // Assert
326
+ expect(experiments).toEqual({})
327
+ consoleSpy.mockRestore()
328
+ })
329
+ })
330
+ })