@tpzdsp/next-toolkit 1.14.2 → 1.15.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.
Files changed (30) hide show
  1. package/package.json +2 -1
  2. package/src/assets/styles/globals.css +5 -1
  3. package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +1 -1
  4. package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +1 -1
  5. package/src/components/ErrorBoundary/ErrorBoundary.tsx +1 -1
  6. package/src/components/InfoBox/InfoBox.tsx +7 -4
  7. package/src/components/accordion/Accordion.test.tsx +5 -10
  8. package/src/components/accordion/Accordion.tsx +4 -7
  9. package/src/components/divider/RuleDivider.test.tsx +4 -4
  10. package/src/components/form/Input.test.tsx +3 -11
  11. package/src/components/form/Input.tsx +2 -2
  12. package/src/components/form/TextArea.test.tsx +3 -5
  13. package/src/components/form/TextArea.tsx +2 -2
  14. package/src/components/layout/header/Header.stories.tsx +3 -3
  15. package/src/components/layout/header/Header.test.tsx +3 -3
  16. package/src/components/layout/header/HeaderNavClient.test.tsx +3 -3
  17. package/src/components/select/Select.stories.tsx +5 -5
  18. package/src/components/select/Select.test.tsx +2 -2
  19. package/src/components/select/Select.tsx +3 -4
  20. package/src/components/select/SelectSkeleton.test.tsx +1 -2
  21. package/src/components/select/SelectSkeleton.tsx +3 -3
  22. package/src/components/select/common.ts +2 -3
  23. package/src/http/constants.ts +4 -0
  24. package/src/http/fetch.ts +2 -0
  25. package/src/http/logger.test.ts +346 -0
  26. package/src/http/logger.ts +412 -76
  27. package/src/map/useKeyboardDrawing.ts +8 -4
  28. package/src/utils/constants.ts +8 -0
  29. package/src/utils/utils.ts +4 -4
  30. package/src/components/theme/ThemeProvider.tsx +0 -30
@@ -0,0 +1,346 @@
1
+ import { Header, HttpMethod, MimeType } from './constants';
2
+ import { requestLogger } from './logger';
3
+
4
+ type MockRequest = {
5
+ method: string;
6
+ url: string;
7
+ body?: unknown;
8
+ headers: Headers;
9
+ meta?: {
10
+ requestId: string;
11
+ };
12
+ retryAttempt?: number;
13
+ loggerOverrides?: {
14
+ requestFormat?: (args: { request: MockRequest }) => string;
15
+ successFormat?: (args: {
16
+ request: MockRequest;
17
+ response: Response;
18
+ data: unknown;
19
+ durationMs: number;
20
+ }) => string;
21
+ errorFormat?: (args: {
22
+ request: MockRequest;
23
+ response?: Response;
24
+ error: unknown;
25
+ durationMs: number;
26
+ }) => string;
27
+ retryFormat?: (args: { request: MockRequest; attempt: number }) => string;
28
+ };
29
+ };
30
+
31
+ const createRequest = (overrides: Partial<MockRequest> = {}): MockRequest => ({
32
+ method: HttpMethod.Post,
33
+ url: 'https://api.example.com/users',
34
+ body: { name: 'Alice' },
35
+ headers: new Headers({
36
+ [Header.ContentType]: MimeType.Json,
37
+ [Header.Accept]: MimeType.Json,
38
+ }),
39
+ ...overrides,
40
+ });
41
+
42
+ const createResponse = (overrides: Partial<Response> = {}): Response =>
43
+ ({
44
+ status: 200,
45
+ headers: new Headers({
46
+ [Header.ContentType]: MimeType.Json,
47
+ }),
48
+ ...overrides,
49
+ }) as Response;
50
+
51
+ describe('requestLogger', () => {
52
+ const nowSpy = vi.spyOn(performance, 'now');
53
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
54
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
55
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
56
+
57
+ beforeEach(() => {
58
+ vi.clearAllMocks();
59
+
60
+ nowSpy.mockReset();
61
+ nowSpy.mockReturnValue(25.0);
62
+
63
+ vi.stubGlobal('crypto', {
64
+ randomUUID: vi.fn(() => 'abc12345-def6-7890-abcd-ef1234567890'),
65
+ });
66
+ });
67
+
68
+ afterEach(() => {
69
+ vi.unstubAllGlobals();
70
+ });
71
+
72
+ it('logs the default request message and sets the request id header', async () => {
73
+ const plugin = requestLogger();
74
+ const request = createRequest();
75
+
76
+ await plugin?.hooks?.onRequest?.(request as never);
77
+
78
+ expect(request.meta?.requestId).toBe('ABC12345');
79
+ expect(request.headers.get(Header.XRequestId)).toBe('ABC12345');
80
+
81
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1);
82
+
83
+ const message = consoleLogSpy.mock.calls[0]?.[0];
84
+
85
+ expect(message).toContain('🚀 [POST] (ABC12345) https://api.example.com/users');
86
+ expect(message).toContain('\t↳ Req. Content-Type: application/json');
87
+ expect(message).toContain('\t↳ Req. Accept: application/json');
88
+ expect(message).toContain('\t↳ Req. Body: {"name":"Alice"}');
89
+ });
90
+
91
+ it('logs the default success message', async () => {
92
+ const plugin = requestLogger();
93
+
94
+ const request = createRequest({
95
+ meta: { requestId: 'ABC12345' },
96
+ });
97
+
98
+ await plugin?.hooks?.onSuccess?.({
99
+ request,
100
+ response: createResponse({ status: 201 }),
101
+ data: { id: 1, name: 'Alice' },
102
+ } as never);
103
+
104
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1);
105
+
106
+ const message = consoleLogSpy.mock.calls[0]?.[0];
107
+
108
+ expect(message).toContain('✅ [POST] (ABC12345) https://api.example.com/users');
109
+ expect(message).toContain('\t↳ 201 | 25.0ms');
110
+ expect(message).toContain('\t↳ Res. Content-Type: application/json');
111
+ expect(message).toContain('\t↳ Res. Body: {"id":1,"name":"Alice"}');
112
+ });
113
+
114
+ it('logs the default retry message', async () => {
115
+ const plugin = requestLogger();
116
+
117
+ const request = createRequest({
118
+ meta: { requestId: 'ABC12345' },
119
+ retryAttempt: 1,
120
+ });
121
+
122
+ await plugin?.hooks?.onRetry?.({
123
+ request,
124
+ } as never);
125
+
126
+ expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
127
+
128
+ const message = consoleWarnSpy.mock.calls[0]?.[0];
129
+
130
+ expect(message).toContain('🔁 [POST] (ABC12345) https://api.example.com/users Retrying...');
131
+ expect(message).toContain('\t↳ Attempt: 2');
132
+ });
133
+
134
+ it('logs the default error message', async () => {
135
+ const plugin = requestLogger();
136
+
137
+ const request = createRequest({
138
+ meta: { requestId: 'ABC12345' },
139
+ });
140
+
141
+ await plugin?.hooks?.onError?.({
142
+ request,
143
+ response: createResponse({ status: 500 }),
144
+ error: { message: 'boom' },
145
+ } as never);
146
+
147
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
148
+
149
+ const message = consoleErrorSpy.mock.calls[0]?.[0];
150
+
151
+ expect(message).toContain('❌ [POST] (ABC12345) https://api.example.com/users');
152
+ expect(message).toContain('\t↳ 500 | 25.0ms');
153
+ expect(message).toContain('\t↳ Error: {"message":"boom"}');
154
+ });
155
+
156
+ it('respects grouped show options', async () => {
157
+ const plugin = requestLogger({
158
+ showMethod: false,
159
+ showUrl: false,
160
+ showAcceptHeader: false,
161
+ showContentType: false,
162
+ showBody: { request: false, response: true },
163
+ showHeaders: { request: true, response: false },
164
+ });
165
+
166
+ const request = createRequest();
167
+
168
+ await plugin?.hooks?.onRequest?.(request as never);
169
+
170
+ const message = consoleLogSpy.mock.calls[0]?.[0];
171
+
172
+ expect(message).toContain('🚀 (ABC12345)');
173
+ expect(message).toContain('\t↳ Req. Headers:');
174
+ expect(message).not.toContain('[POST]');
175
+ expect(message).not.toContain('https://api.example.com/users');
176
+ expect(message).not.toContain('Req. Accept');
177
+ expect(message).not.toContain('Req. Content-Type');
178
+ expect(message).not.toContain('Req. Body');
179
+ });
180
+
181
+ it('prints the full body on a new line when truncateBody is false', async () => {
182
+ const plugin = requestLogger({
183
+ truncateBody: false,
184
+ });
185
+
186
+ const request = createRequest({
187
+ body: {
188
+ very: 'long',
189
+ nested: {
190
+ value: 'hello',
191
+ },
192
+ },
193
+ });
194
+
195
+ await plugin?.hooks?.onRequest?.(request as never);
196
+
197
+ const message = consoleLogSpy.mock.calls[0]?.[0];
198
+
199
+ expect(message).toContain('\t↳ Req. Body:\n{"very":"long","nested":{"value":"hello"}}');
200
+ });
201
+
202
+ it('truncates long bodies by default', async () => {
203
+ const plugin = requestLogger();
204
+
205
+ const request = createRequest({
206
+ body: 'x'.repeat(200),
207
+ });
208
+
209
+ await plugin?.hooks?.onRequest?.(request as never);
210
+
211
+ const message = consoleLogSpy.mock.calls[0]?.[0];
212
+
213
+ expect(message).toContain(`\t↳ Req. Body: ${'x'.repeat(150)}…`);
214
+ });
215
+
216
+ it('uses the request format override when provided', async () => {
217
+ const plugin = requestLogger();
218
+
219
+ const request = createRequest({
220
+ loggerOverrides: {
221
+ requestFormat: ({ request: req }) => `custom request log for ${req.url}`,
222
+ },
223
+ });
224
+
225
+ await plugin?.hooks?.onRequest?.(request as never);
226
+
227
+ expect(consoleLogSpy).toHaveBeenCalledWith(
228
+ 'custom request log for https://api.example.com/users\n',
229
+ );
230
+ });
231
+
232
+ it('uses the success format override when provided', async () => {
233
+ const plugin = requestLogger();
234
+
235
+ const request = createRequest({
236
+ meta: { requestId: 'ABC12345' },
237
+ loggerOverrides: {
238
+ successFormat: ({ response, durationMs }) =>
239
+ `custom success ${response.status} ${durationMs.toFixed(1)}`,
240
+ },
241
+ });
242
+
243
+ await plugin?.hooks?.onSuccess?.({
244
+ request,
245
+ response: createResponse({ status: 204 }),
246
+ data: null,
247
+ } as never);
248
+
249
+ expect(consoleLogSpy).toHaveBeenCalledWith('custom success 204 25.0\n');
250
+ });
251
+
252
+ it('uses the retry format override when provided', async () => {
253
+ const plugin = requestLogger();
254
+
255
+ const request = createRequest({
256
+ meta: { requestId: 'ABC12345' },
257
+ retryAttempt: 2,
258
+ loggerOverrides: {
259
+ retryFormat: ({ attempt }) => `custom retry ${attempt}`,
260
+ },
261
+ });
262
+
263
+ await plugin?.hooks?.onRetry?.({
264
+ request,
265
+ } as never);
266
+
267
+ expect(consoleWarnSpy).toHaveBeenCalledWith('custom retry 3\n');
268
+ });
269
+
270
+ it('uses the error format override when provided', async () => {
271
+ const plugin = requestLogger();
272
+
273
+ const request = createRequest({
274
+ meta: { requestId: 'ABC12345' },
275
+ loggerOverrides: {
276
+ errorFormat: ({ durationMs }) => `custom error ${durationMs.toFixed(1)}`,
277
+ },
278
+ });
279
+
280
+ await plugin?.hooks?.onError?.({
281
+ request,
282
+ response: createResponse({ status: 400 }),
283
+ error: new Error('nope'),
284
+ } as never);
285
+
286
+ expect(consoleErrorSpy).toHaveBeenCalledWith('custom error 25.0\n');
287
+ });
288
+
289
+ it('does not log when disabled is false', async () => {
290
+ const plugin = requestLogger({
291
+ enabled: false,
292
+ });
293
+
294
+ await plugin?.hooks?.onRequest?.(createRequest() as never);
295
+ await plugin?.hooks?.onSuccess?.({
296
+ request: createRequest({
297
+ meta: { requestId: 'ABC12345' },
298
+ }),
299
+ response: createResponse(),
300
+ data: {},
301
+ } as never);
302
+ await plugin?.hooks?.onRetry?.({
303
+ request: createRequest({
304
+ meta: { requestId: 'ABC12345' },
305
+ }),
306
+ } as never);
307
+ await plugin?.hooks?.onError?.({
308
+ request: createRequest({
309
+ meta: { requestId: 'ABC12345' },
310
+ }),
311
+ error: new Error('boom'),
312
+ } as never);
313
+
314
+ expect(consoleLogSpy).not.toHaveBeenCalled();
315
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
316
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
317
+ });
318
+
319
+ it('supports enabled as an async function', async () => {
320
+ const plugin = requestLogger({
321
+ enabled: vi.fn(async () => true),
322
+ });
323
+
324
+ await plugin?.hooks?.onRequest?.(createRequest() as never);
325
+
326
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1);
327
+ });
328
+
329
+ it('uses unknown as the fallback status in error logs when there is no response', async () => {
330
+ const plugin = requestLogger();
331
+
332
+ const request = createRequest({
333
+ meta: { requestId: 'ABC12345' },
334
+ });
335
+
336
+ await plugin?.hooks?.onError?.({
337
+ request,
338
+ error: { message: 'network failed' },
339
+ } as never);
340
+
341
+ const message = consoleErrorSpy.mock.calls[0]?.[0];
342
+
343
+ expect(message).toContain('\t↳ unknown | 25.0ms');
344
+ expect(message).toContain('\t↳ Error: {"message":"network failed"}');
345
+ });
346
+ });