@dxtmisha/functional-basic 0.1.0 → 0.1.1

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.
Files changed (122) hide show
  1. package/package.json +5 -3
  2. package/src/classes/Api.ts +407 -0
  3. package/src/classes/ApiDefault.ts +83 -0
  4. package/src/classes/ApiHeaders.ts +52 -0
  5. package/src/classes/ApiPreparation.ts +114 -0
  6. package/src/classes/ApiResponse.ts +293 -0
  7. package/src/classes/ApiStatus.ts +173 -0
  8. package/src/classes/BroadcastMessage.ts +73 -0
  9. package/src/classes/Cache.ts +60 -0
  10. package/src/classes/CacheItem.ts +95 -0
  11. package/src/classes/CacheStatic.ts +30 -0
  12. package/src/classes/Cookie.ts +135 -0
  13. package/src/classes/CookieBlock.ts +31 -0
  14. package/src/classes/DataStorage.ts +194 -0
  15. package/src/classes/Datetime.ts +891 -0
  16. package/src/classes/EventItem.ts +373 -0
  17. package/src/classes/Geo.ts +320 -0
  18. package/src/classes/GeoFlag.ts +386 -0
  19. package/src/classes/GeoIntl.ts +839 -0
  20. package/src/classes/GeoPhone.ts +272 -0
  21. package/src/classes/Global.ts +32 -0
  22. package/src/classes/Hash.ts +142 -0
  23. package/src/classes/Icons.ts +165 -0
  24. package/src/classes/Loading.ts +90 -0
  25. package/src/classes/Meta.ts +284 -0
  26. package/src/classes/MetaManager.ts +200 -0
  27. package/src/classes/MetaOg.ts +147 -0
  28. package/src/classes/MetaTwitter.ts +154 -0
  29. package/src/classes/ScrollbarWidth.ts +86 -0
  30. package/src/classes/Translate.ts +293 -0
  31. package/src/classes/__tests__/Api.test.ts +728 -0
  32. package/src/classes/__tests__/ApiDefault.test.ts +222 -0
  33. package/src/classes/__tests__/ApiHeaders.test.ts +447 -0
  34. package/src/classes/__tests__/ApiPreparation.test.ts +257 -0
  35. package/src/classes/__tests__/ApiResponse.test.ts +547 -0
  36. package/src/classes/__tests__/ApiStatus.test.ts +403 -0
  37. package/src/classes/__tests__/Meta.test.ts +629 -0
  38. package/src/classes/__tests__/MetaManager.test.ts +836 -0
  39. package/src/classes/__tests__/MetaOg.test.ts +677 -0
  40. package/src/classes/__tests__/MetaTwitter.test.ts +423 -0
  41. package/src/functions/anyToString.ts +36 -0
  42. package/src/functions/applyTemplate.ts +63 -0
  43. package/src/functions/arrFill.ts +10 -0
  44. package/src/functions/copyObject.ts +10 -0
  45. package/src/functions/createElement.ts +40 -0
  46. package/src/functions/domQuerySelector.ts +15 -0
  47. package/src/functions/domQuerySelectorAll.ts +15 -0
  48. package/src/functions/encodeAttribute.ts +15 -0
  49. package/src/functions/eventStopPropagation.ts +10 -0
  50. package/src/functions/executeFunction.ts +13 -0
  51. package/src/functions/executePromise.ts +19 -0
  52. package/src/functions/forEach.ts +39 -0
  53. package/src/functions/frame.ts +38 -0
  54. package/src/functions/getAttributes.ts +27 -0
  55. package/src/functions/getClipboardData.ts +13 -0
  56. package/src/functions/getColumn.ts +18 -0
  57. package/src/functions/getElement.ts +35 -0
  58. package/src/functions/getElementId.ts +39 -0
  59. package/src/functions/getElementItem.ts +27 -0
  60. package/src/functions/getElementOrWindow.ts +23 -0
  61. package/src/functions/getExp.ts +21 -0
  62. package/src/functions/getItemByPath.ts +24 -0
  63. package/src/functions/getKey.ts +9 -0
  64. package/src/functions/getLengthOfAllArray.ts +13 -0
  65. package/src/functions/getMaxLengthAllArray.ts +13 -0
  66. package/src/functions/getMinLengthAllArray.ts +13 -0
  67. package/src/functions/getMouseClient.ts +17 -0
  68. package/src/functions/getMouseClientX.ts +9 -0
  69. package/src/functions/getMouseClientY.ts +9 -0
  70. package/src/functions/getObjectByKeys.ts +24 -0
  71. package/src/functions/getObjectNoUndefined.ts +23 -0
  72. package/src/functions/getObjectOrNone.ts +11 -0
  73. package/src/functions/getRandomText.ts +29 -0
  74. package/src/functions/getRequestString.ts +21 -0
  75. package/src/functions/getStepPercent.ts +19 -0
  76. package/src/functions/getStepValue.ts +19 -0
  77. package/src/functions/goScroll.ts +40 -0
  78. package/src/functions/inArray.ts +10 -0
  79. package/src/functions/initScrollbarOffset.ts +14 -0
  80. package/src/functions/intersectKey.ts +34 -0
  81. package/src/functions/isArray.ts +9 -0
  82. package/src/functions/isDifferent.ts +27 -0
  83. package/src/functions/isDomRuntime.ts +12 -0
  84. package/src/functions/isFilled.ts +49 -0
  85. package/src/functions/isFloat.ts +16 -0
  86. package/src/functions/isFunction.ts +11 -0
  87. package/src/functions/isInDom.ts +15 -0
  88. package/src/functions/isIntegerBetween.ts +11 -0
  89. package/src/functions/isNull.ts +11 -0
  90. package/src/functions/isNumber.ts +16 -0
  91. package/src/functions/isObject.ts +9 -0
  92. package/src/functions/isObjectNotArray.ts +11 -0
  93. package/src/functions/isSelected.ts +32 -0
  94. package/src/functions/isSelectedByList.ts +19 -0
  95. package/src/functions/isString.ts +9 -0
  96. package/src/functions/isWindow.ts +11 -0
  97. package/src/functions/random.ts +10 -0
  98. package/src/functions/replaceRecursive.ts +60 -0
  99. package/src/functions/replaceTemplate.ts +22 -0
  100. package/src/functions/secondToTime.ts +20 -0
  101. package/src/functions/setElementItem.ts +56 -0
  102. package/src/functions/setValues.ts +59 -0
  103. package/src/functions/splice.ts +59 -0
  104. package/src/functions/strFill.ts +12 -0
  105. package/src/functions/toArray.ts +19 -0
  106. package/src/functions/toCamelCase.ts +16 -0
  107. package/src/functions/toCamelCaseFirst.ts +12 -0
  108. package/src/functions/toDate.ts +44 -0
  109. package/src/functions/toKebabCase.ts +25 -0
  110. package/src/functions/toNumber.ts +35 -0
  111. package/src/functions/toNumberByMax.ts +33 -0
  112. package/src/functions/toPercent.ts +10 -0
  113. package/src/functions/toPercentBy100.ts +12 -0
  114. package/src/functions/transformation.ts +59 -0
  115. package/src/functions/uniqueArray.ts +9 -0
  116. package/src/functions/writeClipboardData.ts +17 -0
  117. package/src/library.ts +116 -0
  118. package/src/types/apiTypes.ts +143 -0
  119. package/src/types/basicTypes.ts +155 -0
  120. package/src/types/geoTypes.ts +109 -0
  121. package/src/types/metaTypes.ts +764 -0
  122. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,728 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
6
+ import { Api } from '../Api'
7
+ import { ApiMethodItem } from '../../types/apiTypes'
8
+
9
+ // Mock fetch globally
10
+ const mockFetch = vi.fn()
11
+ globalThis.fetch = mockFetch
12
+
13
+ // Mock console.error to avoid cluttering test output
14
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {
15
+ })
16
+
17
+ describe('Api', () => {
18
+ beforeEach(() => {
19
+ mockFetch.mockClear()
20
+ consoleErrorSpy.mockClear()
21
+
22
+ // Setup default successful response
23
+ mockFetch.mockResolvedValue({
24
+ ok: true,
25
+ status: 200,
26
+ statusText: 'OK',
27
+ headers: new Headers({
28
+ 'Content-Type': 'application/json'
29
+ }),
30
+ json: async () => ({ data: 'test', success: true })
31
+ })
32
+ })
33
+
34
+ afterEach(() => {
35
+ // Reset API state after each test
36
+ Api.setUrl('/api/')
37
+ Api.setHeaders({})
38
+ Api.setRequestDefault({})
39
+ })
40
+
41
+ describe('static configuration methods', () => {
42
+ describe('setUrl', () => {
43
+ it('should set base URL and return Api class', () => {
44
+ const result = Api.setUrl('/custom-api/')
45
+
46
+ expect(result).toBe(Api)
47
+ })
48
+
49
+ it('should use custom URL in requests', async () => {
50
+ Api.setUrl('/custom/')
51
+
52
+ await Api.get({ path: 'test' })
53
+
54
+ expect(mockFetch).toHaveBeenCalledWith(
55
+ expect.stringContaining('/custom/test'),
56
+ expect.any(Object)
57
+ )
58
+ })
59
+ })
60
+
61
+ describe('setHeaders', () => {
62
+ it('should set default headers and return Api class', () => {
63
+ const result = Api.setHeaders({
64
+ 'X-Custom-Header': 'test-value'
65
+ })
66
+
67
+ expect(result).toBe(Api)
68
+ })
69
+
70
+ it('should include default headers in requests', async () => {
71
+ Api.setHeaders({
72
+ 'X-Api-Key': 'test-key-123'
73
+ })
74
+
75
+ await Api.get({ path: 'test' })
76
+
77
+ const fetchCall = mockFetch.mock.calls[0]
78
+ expect(fetchCall?.[1].headers).toMatchObject({
79
+ 'X-Api-Key': 'test-key-123'
80
+ })
81
+ })
82
+ })
83
+
84
+ describe('setRequestDefault', () => {
85
+ it('should set default request data and return Api class', () => {
86
+ const result = Api.setRequestDefault({
87
+ userId: '123',
88
+ token: 'abc'
89
+ })
90
+
91
+ expect(result).toBe(Api)
92
+ })
93
+
94
+ it('should merge default request data with custom request', async () => {
95
+ Api.setRequestDefault({
96
+ apiKey: 'default-key'
97
+ })
98
+
99
+ mockFetch.mockResolvedValueOnce({
100
+ ok: true,
101
+ status: 200,
102
+ statusText: 'OK',
103
+ headers: new Headers({ 'Content-Type': 'application/json' }),
104
+ json: async () => ({ data: 'test' })
105
+ })
106
+
107
+ await Api.post({
108
+ path: 'test',
109
+ request: { customData: 'value' }
110
+ })
111
+
112
+ const body = mockFetch.mock.calls?.[0]?.[1].body
113
+ expect(JSON.parse(body)).toMatchObject({
114
+ apiKey: 'default-key',
115
+ customData: 'value'
116
+ })
117
+ })
118
+ })
119
+
120
+ describe('setPreparation', () => {
121
+ it('should set preparation callback and return Api class', () => {
122
+ const callback = vi.fn().mockResolvedValue(undefined)
123
+ const result = Api.setPreparation(callback)
124
+
125
+ expect(result).toBe(Api)
126
+ })
127
+
128
+ it('should call preparation callback before request', async () => {
129
+ const preparationCallback = vi.fn().mockResolvedValue(undefined)
130
+ Api.setPreparation(preparationCallback)
131
+
132
+ await Api.get({ path: 'test' })
133
+
134
+ expect(preparationCallback).toHaveBeenCalled()
135
+ })
136
+ })
137
+
138
+ describe('setEnd', () => {
139
+ it('should set end callback and return Api class', () => {
140
+ const callback = vi.fn().mockResolvedValue({})
141
+ const result = Api.setEnd(callback)
142
+
143
+ expect(result).toBe(Api)
144
+ })
145
+
146
+ it('should call end callback after request', async () => {
147
+ const endCallback = vi.fn().mockResolvedValue({})
148
+ Api.setEnd(endCallback)
149
+
150
+ await Api.get({ path: 'test' })
151
+
152
+ expect(endCallback).toHaveBeenCalled()
153
+ expect(endCallback.mock.calls?.[0]?.[0]).toBeDefined()
154
+ })
155
+
156
+ it('should retry request when end callback returns reset flag', async () => {
157
+ let callCount = 0
158
+ const endCallback = vi.fn().mockImplementation(async () => {
159
+ callCount++
160
+ if (callCount === 1) {
161
+ return { reset: true }
162
+ }
163
+ return {}
164
+ })
165
+
166
+ Api.setEnd(endCallback)
167
+
168
+ await Api.get({ path: 'test' })
169
+
170
+ expect(endCallback).toHaveBeenCalledTimes(2)
171
+ expect(mockFetch).toHaveBeenCalledTimes(2)
172
+ })
173
+ })
174
+ })
175
+
176
+ describe('utility methods', () => {
177
+ describe('isLocalhost', () => {
178
+ it('should return true when hostname is localhost', () => {
179
+ Object.defineProperty(window, 'location', {
180
+ value: { hostname: 'localhost' },
181
+ writable: true,
182
+ configurable: true
183
+ })
184
+
185
+ expect(Api.isLocalhost()).toBe(true)
186
+ })
187
+
188
+ it('should return false when hostname is not localhost', () => {
189
+ Object.defineProperty(window, 'location', {
190
+ value: { hostname: 'example.com' },
191
+ writable: true,
192
+ configurable: true
193
+ })
194
+
195
+ expect(Api.isLocalhost()).toBe(false)
196
+ })
197
+ })
198
+
199
+ describe('getUrl', () => {
200
+ it('should return path with API prefix by default', () => {
201
+ Api.setUrl('/api/')
202
+ const url = Api.getUrl('users')
203
+
204
+ expect(url).toBe('/api/users')
205
+ })
206
+
207
+ it('should return path without API prefix when api is false', () => {
208
+ const url = Api.getUrl('/custom/path', false)
209
+
210
+ expect(url).toBe('/custom/path')
211
+ })
212
+
213
+ it('should replace {locale} placeholder', () => {
214
+ const url = Api.getUrl('data/{locale}/info')
215
+
216
+ expect(url).toMatch(/data\/\w{2}-\w{2}\/info/)
217
+ })
218
+
219
+ it('should replace {country} placeholder', () => {
220
+ const url = Api.getUrl('data/{country}/info')
221
+
222
+ expect(url).toMatch(/data\/\w{2}\/info/)
223
+ })
224
+
225
+ it('should replace {language} placeholder', () => {
226
+ const url = Api.getUrl('data/{language}/info')
227
+
228
+ expect(url).toMatch(/data\/\w{2}\/info/)
229
+ })
230
+ })
231
+
232
+ describe('getBody', () => {
233
+ it('should return undefined for GET method', () => {
234
+ const body = Api.getBody({ key: 'value' }, ApiMethodItem.get)
235
+
236
+ expect(body).toBeUndefined()
237
+ })
238
+
239
+ it('should return JSON string for POST method with object', () => {
240
+ const body = Api.getBody({ key: 'value' }, ApiMethodItem.post)
241
+
242
+ expect(body).toBe('{"key":"value"}')
243
+ })
244
+
245
+ it('should return FormData as is', () => {
246
+ const formData = new FormData()
247
+ formData.append('key', 'value')
248
+
249
+ const body = Api.getBody(formData, ApiMethodItem.post)
250
+
251
+ expect(body).toBe(formData)
252
+ })
253
+
254
+ it('should return string as is for POST method', () => {
255
+ const body = Api.getBody('custom-string', ApiMethodItem.post)
256
+
257
+ expect(body).toBe('custom-string')
258
+ })
259
+
260
+ it('should return undefined for empty request', () => {
261
+ const body = Api.getBody({}, ApiMethodItem.post)
262
+
263
+ expect(body).toBeUndefined()
264
+ })
265
+ })
266
+
267
+ describe('getBodyForGet', () => {
268
+ it('should return query string for GET method', () => {
269
+ const body = Api.getBodyForGet({ key: 'value', id: 123 }, '/path', ApiMethodItem.get)
270
+
271
+ expect(body).toContain('?')
272
+ expect(body).toContain('key=value')
273
+ expect(body).toContain('id=123')
274
+ })
275
+
276
+ it('should use & when path already has query string', () => {
277
+ const body = Api.getBodyForGet({ key: 'value' }, '/path?existing=param', ApiMethodItem.get)
278
+
279
+ expect(body).toMatch(/^&/)
280
+ })
281
+
282
+ it('should return empty string for non-GET methods', () => {
283
+ const body = Api.getBodyForGet({ key: 'value' }, '/path', ApiMethodItem.post)
284
+
285
+ expect(body).toBe('')
286
+ })
287
+
288
+ it('should return empty string for empty request', () => {
289
+ const body = Api.getBodyForGet({}, '/path', ApiMethodItem.get)
290
+
291
+ expect(body).toBe('')
292
+ })
293
+ })
294
+ })
295
+
296
+ describe('HTTP method shortcuts', () => {
297
+ describe('get', () => {
298
+ it('should make GET request', async () => {
299
+ await Api.get({ path: 'test' })
300
+
301
+ expect(mockFetch).toHaveBeenCalledWith(
302
+ expect.stringContaining('/api/test'),
303
+ expect.objectContaining({ method: ApiMethodItem.get })
304
+ )
305
+ })
306
+
307
+ it('should append query string to URL for GET request', async () => {
308
+ await Api.get({
309
+ path: 'test',
310
+ request: { id: '123', name: 'test' }
311
+ })
312
+
313
+ const url = mockFetch.mock.calls?.[0]?.[0]
314
+ expect(url).toContain('?')
315
+ expect(url).toContain('id=123')
316
+ expect(url).toContain('name=test')
317
+ })
318
+ })
319
+
320
+ describe('post', () => {
321
+ it('should make POST request', async () => {
322
+ await Api.post({
323
+ path: 'test',
324
+ request: { data: 'value' }
325
+ })
326
+
327
+ expect(mockFetch).toHaveBeenCalledWith(
328
+ expect.stringContaining('/api/test'),
329
+ expect.objectContaining({
330
+ method: ApiMethodItem.post,
331
+ body: expect.any(String)
332
+ })
333
+ )
334
+ })
335
+
336
+ it('should send request data in body', async () => {
337
+ await Api.post({
338
+ path: 'test',
339
+ request: { key: 'value' }
340
+ })
341
+
342
+ const body = mockFetch.mock.calls?.[0]?.[1].body
343
+ expect(JSON.parse(body)).toEqual({ key: 'value' })
344
+ })
345
+ })
346
+
347
+ describe('put', () => {
348
+ it('should make PUT request', async () => {
349
+ await Api.put({
350
+ path: 'test',
351
+ request: { data: 'value' }
352
+ })
353
+
354
+ expect(mockFetch).toHaveBeenCalledWith(
355
+ expect.stringContaining('/api/test'),
356
+ expect.objectContaining({ method: ApiMethodItem.put })
357
+ )
358
+ })
359
+ })
360
+
361
+ describe('delete', () => {
362
+ it('should make DELETE request', async () => {
363
+ await Api.delete({ path: 'test' })
364
+
365
+ expect(mockFetch).toHaveBeenCalledWith(
366
+ expect.stringContaining('/api/test'),
367
+ expect.objectContaining({ method: ApiMethodItem.delete })
368
+ )
369
+ })
370
+ })
371
+ })
372
+
373
+ describe('request', () => {
374
+ it('should accept string as path', async () => {
375
+ await Api.request('test/path')
376
+
377
+ expect(mockFetch).toHaveBeenCalledWith(
378
+ expect.stringContaining('/api/test/path'),
379
+ expect.any(Object)
380
+ )
381
+ })
382
+
383
+ it('should accept ApiFetch object', async () => {
384
+ await Api.request({
385
+ path: 'test',
386
+ method: ApiMethodItem.post,
387
+ request: { data: 'value' }
388
+ })
389
+
390
+ expect(mockFetch).toHaveBeenCalledWith(
391
+ expect.stringContaining('/api/test'),
392
+ expect.objectContaining({ method: ApiMethodItem.post })
393
+ )
394
+ })
395
+
396
+ it('should return parsed JSON data by default', async () => {
397
+ mockFetch.mockResolvedValueOnce({
398
+ ok: true,
399
+ status: 200,
400
+ statusText: 'OK',
401
+ headers: new Headers({ 'Content-Type': 'application/json' }),
402
+ json: async () => ({ data: { id: 1, name: 'test' }, success: true })
403
+ })
404
+
405
+ const result = await Api.request<{ id: number, name: string }>({
406
+ path: 'test'
407
+ })
408
+
409
+ expect(result).toEqual({ id: 1, name: 'test', success: true })
410
+ })
411
+
412
+ it('should return full response when toData is false', async () => {
413
+ mockFetch.mockResolvedValueOnce({
414
+ ok: true,
415
+ status: 200,
416
+ statusText: 'OK',
417
+ headers: new Headers({ 'Content-Type': 'application/json' }),
418
+ json: async () => ({ data: 'test', success: true, meta: { count: 10 } })
419
+ })
420
+
421
+ const result = await Api.request({
422
+ path: 'test',
423
+ toData: false
424
+ })
425
+
426
+ expect(result).toEqual({ data: 'test', success: true, meta: { count: 10 } })
427
+ })
428
+
429
+ it('should use custom headers', async () => {
430
+ await Api.request({
431
+ path: 'test',
432
+ headers: {
433
+ Authorization: 'Bearer custom-token'
434
+ }
435
+ })
436
+
437
+ const headers = mockFetch.mock.calls?.[0]?.[1].headers
438
+ expect(headers.Authorization).toBe('Bearer custom-token')
439
+ })
440
+
441
+ it('should use custom content type', async () => {
442
+ await Api.request({
443
+ path: 'test',
444
+ type: 'application/xml'
445
+ })
446
+
447
+ const headers = mockFetch.mock.calls?.[0]?.[1].headers
448
+ expect(headers['Content-Type']).toBe('application/xml')
449
+ })
450
+
451
+ it('should use pathFull when provided', async () => {
452
+ await Api.request({
453
+ pathFull: 'https://external-api.com/data'
454
+ })
455
+
456
+ expect(mockFetch).toHaveBeenCalledWith(
457
+ 'https://external-api.com/data',
458
+ expect.any(Object)
459
+ )
460
+ })
461
+
462
+ it('should pass custom init options to fetch', async () => {
463
+ await Api.request({
464
+ path: 'test',
465
+ init: {
466
+ credentials: 'include',
467
+ mode: 'cors'
468
+ }
469
+ })
470
+
471
+ expect(mockFetch).toHaveBeenCalledWith(
472
+ expect.any(String),
473
+ expect.objectContaining({
474
+ credentials: 'include',
475
+ mode: 'cors'
476
+ })
477
+ )
478
+ })
479
+ })
480
+
481
+ describe('error handling', () => {
482
+ it('should handle fetch errors', async () => {
483
+ mockFetch.mockRejectedValueOnce(new Error('Network error'))
484
+
485
+ const result = await Api.request({ path: 'test' })
486
+
487
+ expect(consoleErrorSpy).toHaveBeenCalled()
488
+ expect(result).toEqual({})
489
+ })
490
+
491
+ it('should not log error when hideError is true', async () => {
492
+ mockFetch.mockRejectedValueOnce(new Error('Network error'))
493
+
494
+ await Api.request({
495
+ path: 'test',
496
+ hideError: true
497
+ })
498
+
499
+ expect(consoleErrorSpy).not.toHaveBeenCalled()
500
+ })
501
+
502
+ it('should set error status on failure', async () => {
503
+ mockFetch.mockRejectedValueOnce(new Error('Request failed'))
504
+
505
+ await Api.request({ path: 'test' })
506
+
507
+ const status = Api.getStatus()
508
+ expect(status.getError()).toContain('Request failed')
509
+ })
510
+ })
511
+
512
+ describe('status tracking', () => {
513
+ it('should update status on successful request', async () => {
514
+ mockFetch.mockResolvedValueOnce({
515
+ ok: true,
516
+ status: 200,
517
+ statusText: 'OK',
518
+ headers: new Headers({ 'Content-Type': 'application/json' }),
519
+ json: async () => ({ data: 'test' })
520
+ })
521
+
522
+ await Api.request({ path: 'test' })
523
+
524
+ const status = Api.getStatus()
525
+ expect(status.getStatus()).toBe(200)
526
+ expect(status.getStatusText()).toBe('OK')
527
+ })
528
+
529
+ it('should update status on 404 response', async () => {
530
+ mockFetch.mockResolvedValueOnce({
531
+ ok: false,
532
+ status: 404,
533
+ statusText: 'Not Found',
534
+ headers: new Headers({ 'Content-Type': 'application/json' }),
535
+ json: async () => ({ error: 'Not found' })
536
+ })
537
+
538
+ await Api.request({ path: 'test' })
539
+
540
+ const status = Api.getStatus()
541
+ expect(status.getStatus()).toBe(404)
542
+ expect(status.getStatusText()).toBe('Not Found')
543
+ })
544
+
545
+ it('should store last response', async () => {
546
+ const responseData = { data: { id: 1, name: 'test' }, success: true }
547
+ mockFetch.mockResolvedValueOnce({
548
+ ok: true,
549
+ status: 200,
550
+ statusText: 'OK',
551
+ headers: new Headers({ 'Content-Type': 'application/json' }),
552
+ json: async () => responseData
553
+ })
554
+
555
+ await Api.request({ path: 'test' })
556
+
557
+ const status = Api.getStatus()
558
+ expect(status.getResponse()).toMatchObject(responseData)
559
+ })
560
+ })
561
+
562
+ describe('custom response processing', () => {
563
+ it('should use queryReturn callback when provided', async () => {
564
+ const customCallback = vi.fn().mockResolvedValue({ customData: 'processed' })
565
+
566
+ mockFetch.mockResolvedValueOnce({
567
+ ok: true,
568
+ status: 200,
569
+ statusText: 'OK',
570
+ headers: new Headers({ 'Content-Type': 'application/json' }),
571
+ json: async () => ({ data: 'original' })
572
+ })
573
+
574
+ const result = await Api.request({
575
+ path: 'test',
576
+ queryReturn: customCallback
577
+ })
578
+
579
+ expect(customCallback).toHaveBeenCalled()
580
+ expect(result).toEqual({ customData: 'processed' })
581
+ })
582
+
583
+ it('should handle text responses', async () => {
584
+ mockFetch.mockResolvedValueOnce({
585
+ ok: true,
586
+ status: 200,
587
+ statusText: 'OK',
588
+ headers: new Headers({ 'Content-Type': 'text/plain' }),
589
+ text: async () => 'plain text response'
590
+ })
591
+
592
+ const result = await Api.request({ path: 'test' })
593
+
594
+ expect(result).toBe('plain text response')
595
+ })
596
+ })
597
+
598
+ describe('preparation lifecycle', () => {
599
+ it('should not call global preparation when globalPreparation is false', async () => {
600
+ const preparationCallback = vi.fn().mockResolvedValue(undefined)
601
+ Api.setPreparation(preparationCallback)
602
+
603
+ await Api.request({
604
+ path: 'test',
605
+ globalPreparation: false
606
+ })
607
+
608
+ expect(preparationCallback).not.toHaveBeenCalled()
609
+ })
610
+
611
+ it('should not call global end when globalEnd is false', async () => {
612
+ const endCallback = vi.fn().mockResolvedValue({})
613
+ Api.setEnd(endCallback)
614
+
615
+ await Api.request({
616
+ path: 'test',
617
+ globalEnd: false
618
+ })
619
+
620
+ expect(endCallback).not.toHaveBeenCalled()
621
+ })
622
+
623
+ it('should use data from end callback when provided', async () => {
624
+ const endCallback = vi.fn().mockResolvedValue({
625
+ data: { customData: 'from-end-callback' }
626
+ })
627
+ Api.setEnd(endCallback)
628
+
629
+ const result = await Api.request({ path: 'test' })
630
+
631
+ expect(result).toEqual({ customData: 'from-end-callback' })
632
+ })
633
+ })
634
+
635
+ describe('integration scenarios', () => {
636
+ it('should handle complete request lifecycle', async () => {
637
+ const preparationCallback = vi.fn().mockResolvedValue(undefined)
638
+ const endCallback = vi.fn().mockResolvedValue({})
639
+
640
+ Api.setUrl('/api/v1/')
641
+ Api.setHeaders({ 'X-Api-Key': 'test-key' })
642
+ Api.setPreparation(preparationCallback)
643
+ Api.setEnd(endCallback)
644
+
645
+ mockFetch.mockResolvedValueOnce({
646
+ ok: true,
647
+ status: 200,
648
+ statusText: 'OK',
649
+ headers: new Headers({ 'Content-Type': 'application/json' }),
650
+ json: async () => ({ data: { userId: 1 }, success: true })
651
+ })
652
+
653
+ const result = await Api.post({
654
+ path: 'users',
655
+ request: { name: 'John' }
656
+ })
657
+
658
+ expect(preparationCallback).toHaveBeenCalled()
659
+ expect(endCallback).toHaveBeenCalled()
660
+ expect(mockFetch).toHaveBeenCalledWith(
661
+ expect.stringContaining('/api/v1/users'),
662
+ expect.objectContaining({
663
+ method: ApiMethodItem.post,
664
+ headers: expect.objectContaining({
665
+ 'X-Api-Key': 'test-key'
666
+ })
667
+ })
668
+ )
669
+ expect(result).toEqual({ userId: 1, success: true })
670
+ })
671
+
672
+ it('should handle FormData upload', async () => {
673
+ const formData = new FormData()
674
+ formData.append('file', new Blob(['test'], { type: 'text/plain' }))
675
+ formData.append('name', 'test-file')
676
+
677
+ await Api.post({
678
+ path: 'upload',
679
+ request: formData
680
+ })
681
+
682
+ const fetchCall = mockFetch.mock.calls[0]
683
+ expect(fetchCall?.[1].body).toBe(formData)
684
+ })
685
+
686
+ it('should handle retry logic with reset flag', async () => {
687
+ let attempts = 0
688
+ const endCallback = vi.fn().mockImplementation(async () => {
689
+ attempts++
690
+ if (attempts === 1) {
691
+ return { reset: true }
692
+ }
693
+ return {}
694
+ })
695
+
696
+ Api.setEnd(endCallback)
697
+
698
+ mockFetch.mockResolvedValue({
699
+ ok: true,
700
+ status: 200,
701
+ statusText: 'OK',
702
+ headers: new Headers({ 'Content-Type': 'application/json' }),
703
+ json: async () => ({ data: 'success' })
704
+ })
705
+
706
+ await Api.request({ path: 'test' })
707
+
708
+ expect(attempts).toBe(2)
709
+ expect(mockFetch).toHaveBeenCalledTimes(2)
710
+ })
711
+ })
712
+
713
+ describe('getStatus and getResponse', () => {
714
+ it('should return status instance', () => {
715
+ const status = Api.getStatus()
716
+
717
+ expect(status).toBeDefined()
718
+ expect(typeof status.getStatus).toBe('function')
719
+ })
720
+
721
+ it('should return response instance', () => {
722
+ const response = Api.getResponse()
723
+
724
+ expect(response).toBeDefined()
725
+ expect(typeof response.emulator).toBe('function')
726
+ })
727
+ })
728
+ })