@logtape/pretty 1.3.1 → 1.3.2

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.
@@ -1,954 +0,0 @@
1
- import { suite } from "@alinea/suite";
2
- import type { LogRecord } from "@logtape/logtape";
3
- import { assert } from "@std/assert/assert";
4
- import { assertEquals } from "@std/assert/equals";
5
- import { assertMatch } from "@std/assert/match";
6
- import { assertStringIncludes } from "@std/assert/string-includes";
7
- import {
8
- type CategoryColorMap,
9
- getPrettyFormatter,
10
- prettyFormatter,
11
- } from "./formatter.ts";
12
-
13
- const test = suite(import.meta);
14
-
15
- function createLogRecord(
16
- level: LogRecord["level"],
17
- category: string[],
18
- message: LogRecord["message"],
19
- timestamp: number = Date.now(),
20
- properties: Record<string, unknown> = {},
21
- ): LogRecord {
22
- // Convert message array to template strings format for rawMessage
23
- const rawMessage = typeof message === "string"
24
- ? message
25
- : message.filter((_, i) => i % 2 === 0).join("{}");
26
-
27
- return {
28
- level,
29
- category,
30
- message,
31
- rawMessage,
32
- properties,
33
- timestamp,
34
- };
35
- }
36
-
37
- test("prettyFormatter basic output", () => {
38
- const record = createLogRecord(
39
- "info",
40
- ["app", "server"],
41
- ["Server started on port ", 3000],
42
- );
43
-
44
- const output = prettyFormatter(record);
45
-
46
- // Should contain emoji, level, category, and message
47
- assertMatch(output, /✨/);
48
- assertMatch(output, /info/); // Default level format is "full"
49
- assertMatch(output, /app·server/);
50
- assertMatch(output, /Server started on port/);
51
- assertMatch(output, /3000/);
52
- });
53
-
54
- test("getPrettyFormatter() with no colors", () => {
55
- const formatter = getPrettyFormatter({ colors: false });
56
- const record = createLogRecord(
57
- "error",
58
- ["app", "auth"],
59
- ["Authentication failed"],
60
- );
61
-
62
- const output = formatter(record);
63
-
64
- // Should not contain ANSI escape codes
65
- assertEquals(output.includes("\x1b["), false);
66
- assertMatch(output, /❌ error/); // Default level format is "full"
67
- assertMatch(output, /app·auth/);
68
- });
69
-
70
- test("getPrettyFormatter() with custom icons", () => {
71
- const formatter = getPrettyFormatter({
72
- icons: {
73
- info: "ℹ️ ",
74
- error: "🔥",
75
- },
76
- });
77
-
78
- const infoRecord = createLogRecord("info", ["test"], ["Info message"]);
79
- const errorRecord = createLogRecord("error", ["test"], ["Error message"]);
80
-
81
- assertMatch(formatter(infoRecord), /ℹ️/);
82
- assertMatch(formatter(errorRecord), /🔥/);
83
- });
84
-
85
- test("getPrettyFormatter() with no icons", () => {
86
- const formatter = getPrettyFormatter({ icons: false });
87
- const record = createLogRecord("info", ["test"], ["Message"]);
88
-
89
- const output = formatter(record);
90
-
91
- // Should not contain any emoji
92
- assertEquals(output.includes("✨"), false);
93
- assertEquals(output.includes("🐛"), false);
94
- });
95
-
96
- test("getPrettyFormatter() with timestamp", () => {
97
- const timestamp = new Date("2024-01-15T12:34:56Z").getTime();
98
-
99
- // Time only - note UTC timezone handling
100
- const timeFormatter = getPrettyFormatter({ timestamp: "time" });
101
- const record = createLogRecord("info", ["test"], ["Message"], timestamp);
102
- const timeOutput = timeFormatter(record);
103
- assertMatch(timeOutput, /\d{2}:\d{2}:\d{2}/);
104
-
105
- // Date and time
106
- const datetimeFormatter = getPrettyFormatter({ timestamp: "date-time" });
107
- const datetimeOutput = datetimeFormatter(record);
108
- assertMatch(datetimeOutput, /2024-01-15/);
109
- assertMatch(datetimeOutput, /\d{2}:\d{2}:\d{2}/);
110
-
111
- // Custom formatter
112
- const customFormatter = getPrettyFormatter({
113
- timestamp: (ts) => new Date(ts).toISOString(),
114
- });
115
- const customOutput = customFormatter(record);
116
- assertMatch(customOutput, /2024-01-15T12:34:56/);
117
-
118
- // Test function returning null
119
- const nullFormatter = getPrettyFormatter({
120
- timestamp: () => null,
121
- });
122
- const nullOutput = nullFormatter(record);
123
- assertEquals(nullOutput.includes("2024"), false);
124
-
125
- // Test none timestamp
126
- const noneFormatter = getPrettyFormatter({ timestamp: "none" });
127
- const noneOutput = noneFormatter(record);
128
- assertEquals(noneOutput.includes("2024"), false);
129
- });
130
-
131
- test("getPrettyFormatter() category truncation", () => {
132
- const formatter = getPrettyFormatter({
133
- categoryWidth: 15,
134
- categoryTruncate: "middle",
135
- });
136
-
137
- const record = createLogRecord(
138
- "info",
139
- ["app", "server", "http", "middleware"],
140
- ["Request processed"],
141
- );
142
-
143
- const output = formatter(record);
144
- // Category should be truncated and contain app
145
- assertMatch(output, /app/);
146
- assertMatch(output, /…/);
147
- });
148
-
149
- test("getPrettyFormatter() with null colors", () => {
150
- const formatter = getPrettyFormatter({
151
- levelColors: {
152
- info: null, // No color
153
- },
154
- categoryColor: null,
155
- });
156
-
157
- const record = createLogRecord("info", ["test"], ["Message"]);
158
- const result = formatter(record);
159
- // Should work without errors and have basic formatting
160
- assertStringIncludes(result, "info"); // Default level format is "full"
161
- assertStringIncludes(result, "test");
162
- assertStringIncludes(result, "Message");
163
- });
164
-
165
- test("getPrettyFormatter() with values", () => {
166
- const formatter = getPrettyFormatter();
167
- const record = createLogRecord(
168
- "debug",
169
- ["app"],
170
- ["User data: ", { id: 123, name: "John" }, ", array: ", [1, 2, 3]],
171
- );
172
-
173
- const output = formatter(record);
174
- assertMatch(output, /User data:/);
175
- assertMatch(output, /123/);
176
- assertMatch(output, /John/);
177
- assertMatch(output, /1.*2.*3/);
178
- });
179
-
180
- test("getPrettyFormatter() all log levels", () => {
181
- const formatter = getPrettyFormatter();
182
-
183
- const levels: LogRecord["level"][] = [
184
- "trace",
185
- "debug",
186
- "info",
187
- "warning",
188
- "error",
189
- "fatal",
190
- ];
191
- const expectedIcons = ["🔍", "🐛", "✨", "⚡", "❌", "💀"];
192
-
193
- levels.forEach((level, i) => {
194
- const record = createLogRecord(level, ["test"], [`${level} message`]);
195
- const output = formatter(record);
196
-
197
- assertMatch(output, new RegExp(expectedIcons[i]));
198
- // Check for full level format (default)
199
- assertMatch(output, new RegExp(level));
200
- });
201
- });
202
-
203
- test("getPrettyFormatter() alignment", () => {
204
- const formatter = getPrettyFormatter({ align: true, colors: false });
205
-
206
- const records = [
207
- createLogRecord("info", ["app"], ["Short"]),
208
- createLogRecord("warning", ["app"], ["Longer level"]),
209
- ];
210
-
211
- const outputs = records.map((r) => formatter(r));
212
-
213
- // With alignment, warning (longer) should have more padding before the category
214
- // Just check that both outputs contain the expected content
215
- assertMatch(outputs[0], /✨ info.*app.*Short/); // Default level format is "full"
216
- assertMatch(outputs[1], /⚡.*warning.*app.*Longer level/); // Default level format is "full"
217
- });
218
-
219
- test("getPrettyFormatter() no alignment", () => {
220
- const formatter = getPrettyFormatter({ align: false, colors: false });
221
-
222
- const record = createLogRecord("info", ["app"], ["Message"]);
223
- const output = formatter(record);
224
-
225
- // Should still be formatted but without padding
226
- assertMatch(output, /✨ info app Message/); // Default level format is "full"
227
- });
228
-
229
- test("getPrettyFormatter() with hex colors", () => {
230
- const formatter = getPrettyFormatter({
231
- levelColors: {
232
- info: "#00ff00", // Bright green
233
- error: "#ff0000", // Bright red
234
- },
235
- categoryColor: "#888888",
236
- messageColor: "#cccccc",
237
- });
238
-
239
- const record = createLogRecord("info", ["test"], ["Message"]);
240
- const result = formatter(record);
241
- // Should contain true color ANSI codes for hex colors
242
- assertStringIncludes(result, "\x1b[38;2;0;255;0m"); // #00ff00 converted to RGB
243
- });
244
-
245
- test("getPrettyFormatter() with rgb colors", () => {
246
- const formatter = getPrettyFormatter({
247
- levelColors: {
248
- info: "rgb(255,128,0)", // Orange
249
- },
250
- timestampColor: "rgb(100,100,100)",
251
- timestamp: "time",
252
- });
253
-
254
- const record = createLogRecord("info", ["test"], ["Message"]);
255
- const result = formatter(record);
256
- // Should contain true color ANSI codes for RGB colors
257
- assertStringIncludes(result, "\x1b[38;2;255;128;0m"); // rgb(255,128,0)
258
- assertStringIncludes(result, "\x1b[38;2;100;100;100m"); // timestamp color
259
- });
260
-
261
- test("getPrettyFormatter() with level formats", () => {
262
- const abbr = getPrettyFormatter({ level: "ABBR" });
263
- const full = getPrettyFormatter({ level: "FULL" });
264
- const letter = getPrettyFormatter({ level: "L" });
265
- const custom = getPrettyFormatter({ level: (level) => `[${level}]` });
266
-
267
- const record = createLogRecord("info", ["test"], ["Message"]);
268
- const abbrResult = abbr(record);
269
- const fullResult = full(record);
270
- const letterResult = letter(record);
271
- const customResult = custom(record);
272
-
273
- assertStringIncludes(abbrResult, "INF");
274
- assertStringIncludes(fullResult, "INFO");
275
- assertStringIncludes(letterResult, "I");
276
- assertStringIncludes(customResult, "[info]");
277
- });
278
-
279
- test("getPrettyFormatter() with extended timestamp formats", () => {
280
- const timestamp = new Date("2023-05-15T10:30:00.000Z").getTime();
281
- const record = createLogRecord("info", ["test"], ["Message"], timestamp);
282
-
283
- // Test all TextFormatterOptions timestamp formats
284
- const dateTimeTimezone = getPrettyFormatter({
285
- timestamp: "date-time-timezone",
286
- });
287
- const dateTimeTz = getPrettyFormatter({ timestamp: "date-time-tz" });
288
- const dateTime = getPrettyFormatter({ timestamp: "date-time" });
289
- const timeTimezone = getPrettyFormatter({ timestamp: "time-timezone" });
290
- const timeTz = getPrettyFormatter({ timestamp: "time-tz" });
291
- const rfc3339 = getPrettyFormatter({ timestamp: "rfc3339" });
292
- const dateOnly = getPrettyFormatter({ timestamp: "date" });
293
- const datetime = getPrettyFormatter({ timestamp: "date-time" });
294
- const none = getPrettyFormatter({ timestamp: "none" });
295
- const disabled = getPrettyFormatter({ timestamp: "disabled" });
296
-
297
- const dateTimeTimezoneResult = dateTimeTimezone(record);
298
- const dateTimeTzResult = dateTimeTz(record);
299
- const dateTimeResult = dateTime(record);
300
- const timeTimezoneResult = timeTimezone(record);
301
- const timeTzResult = timeTz(record);
302
- const rfc3339Result = rfc3339(record);
303
- const dateOnlyResult = dateOnly(record);
304
- const datetimeResult = datetime(record);
305
- const noneResult = none(record);
306
- const disabledResult = disabled(record);
307
-
308
- // Check that appropriate timestamps are included
309
- assertStringIncludes(dateTimeTimezoneResult, "2023-05-15");
310
- assertStringIncludes(dateTimeTimezoneResult, "+00:00");
311
- assertStringIncludes(dateTimeTzResult, "2023-05-15");
312
- assertStringIncludes(dateTimeTzResult, "+00");
313
- assertStringIncludes(dateTimeResult, "2023-05-15");
314
- assertStringIncludes(timeTimezoneResult, "10:30:00");
315
- assertStringIncludes(timeTzResult, "10:30:00");
316
- assertStringIncludes(rfc3339Result, "2023-05-15T10:30:00.000Z");
317
- assertStringIncludes(dateOnlyResult, "2023-05-15");
318
- assertStringIncludes(datetimeResult, "2023-05-15 10:30:00");
319
-
320
- // Check that none/disabled don't include timestamps
321
- assertEquals(noneResult.includes("2023"), false);
322
- assertEquals(disabledResult.includes("2023"), false);
323
- });
324
-
325
- test("getPrettyFormatter() with styles", () => {
326
- const formatter = getPrettyFormatter({
327
- levelStyle: "bold",
328
- categoryStyle: "italic",
329
- messageStyle: "underline",
330
- timestampStyle: "strikethrough",
331
- timestamp: "time",
332
- });
333
-
334
- const record = createLogRecord("info", ["test"], ["Message"]);
335
- const result = formatter(record);
336
- // Should contain ANSI style codes
337
- assertStringIncludes(result, "\x1b[1m"); // bold
338
- assertStringIncludes(result, "\x1b[3m"); // italic
339
- assertStringIncludes(result, "\x1b[4m"); // underline
340
- assertStringIncludes(result, "\x1b[9m"); // strikethrough
341
- });
342
-
343
- test("getPrettyFormatter() with custom category separator", () => {
344
- const formatter = getPrettyFormatter({
345
- categorySeparator: ">",
346
- colors: false,
347
- });
348
-
349
- const record = createLogRecord("info", ["app", "web", "server"], ["Message"]);
350
- const result = formatter(record);
351
- assertStringIncludes(result, "app>web>server");
352
- });
353
-
354
- test("getPrettyFormatter() with ANSI colors", () => {
355
- const formatter = getPrettyFormatter({
356
- levelColors: {
357
- info: "green",
358
- error: "red",
359
- },
360
- categoryColor: "blue",
361
- });
362
-
363
- const record = createLogRecord("info", ["test"], ["Message"]);
364
- const result = formatter(record);
365
- // Should contain ANSI color codes
366
- assertStringIncludes(result, "\x1b[32m"); // green
367
- assertStringIncludes(result, "\x1b[34m"); // blue
368
- });
369
-
370
- test("Color helper functions with 3-digit hex", () => {
371
- const formatter = getPrettyFormatter({
372
- levelColors: {
373
- info: "#fff", // 3-digit hex
374
- },
375
- });
376
-
377
- const record = createLogRecord("info", ["test"], ["Message"]);
378
- const result = formatter(record);
379
- // Should contain converted RGB codes
380
- assertStringIncludes(result, "\x1b[38;2;255;255;255m"); // #fff -> rgb(255,255,255)
381
- });
382
-
383
- test("getPrettyFormatter() with category color mapping", () => {
384
- const categoryColorMap: CategoryColorMap = new Map([
385
- [["app", "auth"], "#ff6b6b"], // red for app.auth.*
386
- [["app", "db"], "#4ecdc4"], // teal for app.db.*
387
- [["app"], "#45b7d1"], // blue for app.* (fallback)
388
- [["lib"], "#96ceb4"], // green for lib.*
389
- ]);
390
-
391
- const formatter = getPrettyFormatter({
392
- categoryColorMap,
393
- colors: true,
394
- });
395
-
396
- // Test exact match
397
- const authRecord = createLogRecord("info", ["app", "auth", "login"], [
398
- "User logged in",
399
- ]);
400
- const authResult = formatter(authRecord);
401
- assertStringIncludes(authResult, "\x1b[38;2;255;107;107m"); // #ff6b6b
402
-
403
- // Test prefix fallback
404
- const miscRecord = createLogRecord("info", ["app", "utils"], [
405
- "Utility called",
406
- ]);
407
- const miscResult = formatter(miscRecord);
408
- assertStringIncludes(miscResult, "\x1b[38;2;69;183;209m"); // #45b7d1
409
-
410
- // Test different prefix
411
- const libRecord = createLogRecord("info", ["lib", "http"], ["HTTP request"]);
412
- const libResult = formatter(libRecord);
413
- assertStringIncludes(libResult, "\x1b[38;2;150;206;180m"); // #96ceb4
414
- });
415
-
416
- test("Category color mapping precedence", () => {
417
- const categoryColorMap: CategoryColorMap = new Map([
418
- [["app", "auth", "jwt"], "#ff0000"], // Most specific
419
- [["app", "auth"], "#00ff00"], // Less specific
420
- [["app"], "#0000ff"], // Least specific
421
- ]);
422
-
423
- const formatter = getPrettyFormatter({
424
- categoryColorMap,
425
- colors: true,
426
- });
427
-
428
- // Should match most specific pattern
429
- const jwtRecord = createLogRecord("info", ["app", "auth", "jwt", "verify"], [
430
- "Token verified",
431
- ]);
432
- const jwtResult = formatter(jwtRecord);
433
- assertStringIncludes(jwtResult, "\x1b[38;2;255;0;0m"); // #ff0000
434
-
435
- // Should match less specific pattern
436
- const authRecord = createLogRecord("info", ["app", "auth", "session"], [
437
- "Session created",
438
- ]);
439
- const authResult = formatter(authRecord);
440
- assertStringIncludes(authResult, "\x1b[38;2;0;255;0m"); // #00ff00
441
-
442
- // Should match least specific pattern
443
- const appRecord = createLogRecord("info", ["app", "server"], [
444
- "Server started",
445
- ]);
446
- const appResult = formatter(appRecord);
447
- assertStringIncludes(appResult, "\x1b[38;2;0;0;255m"); // #0000ff
448
- });
449
-
450
- test("Category color mapping with no match", () => {
451
- const categoryColorMap: CategoryColorMap = new Map([
452
- [["app"], "#ff0000"],
453
- ]);
454
-
455
- const formatter = getPrettyFormatter({
456
- categoryColorMap,
457
- categoryColor: "#00ff00", // fallback color
458
- colors: true,
459
- });
460
-
461
- // Should use fallback color for non-matching category
462
- const record = createLogRecord("info", ["system", "kernel"], [
463
- "Kernel message",
464
- ]);
465
- const result = formatter(record);
466
- assertStringIncludes(result, "\x1b[38;2;0;255;0m"); // fallback #00ff00
467
- });
468
-
469
- test("Interpolated values with proper color reset/reapply", () => {
470
- const formatter = getPrettyFormatter({
471
- messageColor: "#ffffff",
472
- messageStyle: "dim",
473
- colors: true,
474
- });
475
-
476
- const record = createLogRecord("info", ["test"], [
477
- "User data: ",
478
- { id: 123, name: "John" },
479
- ", status: ",
480
- "active",
481
- ]);
482
-
483
- const result = formatter(record);
484
-
485
- // Should contain proper color reset/reapply around interpolated values
486
- // The exact ANSI codes depend on inspect() output, but we should see resets
487
- assertStringIncludes(result, "\x1b[0m"); // Reset code should be present
488
- assertStringIncludes(result, "\x1b[2m"); // Dim style should be reapplied
489
- assertStringIncludes(result, "\x1b[38;2;255;255;255m"); // White color should be reapplied
490
- });
491
-
492
- test("Multiple styles combination", () => {
493
- const formatter = getPrettyFormatter({
494
- levelStyle: ["bold", "underline"],
495
- categoryStyle: ["dim", "italic"],
496
- messageStyle: ["bold", "strikethrough"],
497
- timestampStyle: ["dim", "underline"],
498
- timestamp: "time",
499
- colors: true,
500
- });
501
-
502
- const record = createLogRecord("info", ["test"], ["Message"]);
503
- const result = formatter(record);
504
-
505
- // Should contain multiple ANSI style codes combined
506
- assertStringIncludes(result, "\x1b[1m"); // bold
507
- assertStringIncludes(result, "\x1b[4m"); // underline
508
- assertStringIncludes(result, "\x1b[2m"); // dim
509
- assertStringIncludes(result, "\x1b[3m"); // italic
510
- assertStringIncludes(result, "\x1b[9m"); // strikethrough
511
- });
512
-
513
- ("Bun" in globalThis ? test.skip : test)(
514
- "Word wrapping enabled by default",
515
- () => {
516
- const formatter = getPrettyFormatter({
517
- colors: false,
518
- });
519
-
520
- const longMessage =
521
- "This is a very long message that would normally exceed the typical console width and should be wrapped when word wrapping is enabled by default.";
522
- const record = createLogRecord("info", ["test"], [longMessage]);
523
- const result = formatter(record);
524
-
525
- // Should contain multiple line breaks due to wrapping
526
- const lines = result.split("\n");
527
- assert(lines.length > 2); // More than just content + trailing newline due to wrapping
528
-
529
- // First line should contain the beginning of the message
530
- assert(lines[0].includes("This is a very long message"));
531
- },
532
- );
533
-
534
- test("Word wrapping can be disabled", () => {
535
- const formatter = getPrettyFormatter({
536
- colors: false,
537
- wordWrap: false,
538
- });
539
-
540
- const longMessage =
541
- "This is a very long message that would normally exceed the typical console width but should not be wrapped when word wrapping is explicitly disabled.";
542
- const record = createLogRecord("info", ["test"], [longMessage]);
543
- const result = formatter(record);
544
-
545
- // Should not contain any line breaks in the message (only the trailing newline)
546
- const lines = result.split("\n");
547
- assertEquals(lines.length, 2); // One content line + one empty line from trailing newline
548
- assertStringIncludes(lines[0], longMessage);
549
- });
550
-
551
- test("Word wrapping with 80", () => {
552
- const formatter = getPrettyFormatter({
553
- wordWrap: 80,
554
- colors: false,
555
- align: false,
556
- });
557
-
558
- const longMessage =
559
- "This is a very long message that should be wrapped at approximately 80 characters when word wrapping is enabled with the default width setting.";
560
- const record = createLogRecord("info", ["test"], [longMessage]);
561
- const result = formatter(record);
562
-
563
- // Should contain multiple lines due to wrapping
564
- const lines = result.split("\n");
565
- assert(lines.length > 2); // More than just content + trailing newline
566
-
567
- // Each content line should be roughly within the wrap width
568
- const contentLines = lines.filter((line) => line.length > 0);
569
- for (const line of contentLines) {
570
- assert(line.length <= 85); // Allow some tolerance for word boundaries
571
- }
572
- });
573
-
574
- test("Word wrapping with custom width", () => {
575
- const formatter = getPrettyFormatter({
576
- wordWrap: 40,
577
- colors: false,
578
- align: false,
579
- });
580
-
581
- const longMessage =
582
- "This is a message that should be wrapped at 40 characters maximum width.";
583
- const record = createLogRecord("info", ["test"], [longMessage]);
584
- const result = formatter(record);
585
-
586
- // Should contain multiple lines due to aggressive wrapping
587
- const lines = result.split("\n");
588
- assert(lines.length > 2);
589
-
590
- // Each content line should be within 40 characters
591
- const contentLines = lines.filter((line) => line.length > 0);
592
- for (const line of contentLines) {
593
- assert(line.length <= 45); // Allow some tolerance
594
- }
595
- });
596
-
597
- test("Word wrapping with proper indentation", () => {
598
- const formatter = getPrettyFormatter({
599
- wordWrap: 50,
600
- colors: false,
601
- align: false,
602
- });
603
-
604
- const longMessage =
605
- "This is a long message that should wrap with proper indentation to align with the message column.";
606
- const record = createLogRecord("info", ["app"], [longMessage]);
607
- const result = formatter(record);
608
-
609
- const lines = result.split("\n");
610
- const contentLines = lines.filter((line) => line.length > 0);
611
-
612
- // Should have multiple lines due to wrapping
613
- assert(contentLines.length > 1);
614
-
615
- // First line starts with icon
616
- assert(contentLines[0].startsWith("✨ info"));
617
-
618
- // Check that lines are properly wrapped at word boundaries
619
- // With align: false, the format should be "✨ info app message..."
620
- // and continuation lines should be properly indented
621
- assert(
622
- contentLines.length >= 2,
623
- "Should have at least 2 lines from wrapping",
624
- );
625
- });
626
-
627
- test("getPrettyFormatter() with consistent icon spacing", () => {
628
- // Test with custom icons of different display widths
629
- const formatter = getPrettyFormatter({
630
- icons: {
631
- info: "ℹ️", // 2 width emoji
632
- warning: "!", // 1 width character
633
- error: "🚨🚨", // 4 width (2 emojis)
634
- },
635
- colors: false,
636
- align: true,
637
- wordWrap: 50,
638
- });
639
-
640
- const longMessage = "This is a long message that should wrap consistently";
641
-
642
- const infoRecord = createLogRecord("info", ["test"], [longMessage]);
643
- const warningRecord = createLogRecord("warning", ["test"], [longMessage]);
644
- const errorRecord = createLogRecord("error", ["test"], [longMessage]);
645
-
646
- const infoResult = formatter(infoRecord);
647
- const warningResult = formatter(warningRecord);
648
- const errorResult = formatter(errorRecord);
649
-
650
- // Split into lines and get continuation lines
651
- const infoLines = infoResult.split("\n").filter((line) => line.length > 0);
652
- const warningLines = warningResult.split("\n").filter((line) =>
653
- line.length > 0
654
- );
655
- const errorLines = errorResult.split("\n").filter((line) => line.length > 0);
656
-
657
- // All should have multiple lines due to wrapping
658
- assert(infoLines.length > 1, "Info should wrap to multiple lines");
659
- assert(warningLines.length > 1, "Warning should wrap to multiple lines");
660
- assert(errorLines.length > 1, "Error should wrap to multiple lines");
661
-
662
- // Check that continuation lines are indented to the same position
663
- // despite different icon widths
664
- if (
665
- infoLines.length > 1 && warningLines.length > 1 && errorLines.length > 1
666
- ) {
667
- const infoIndent = infoLines[1].search(/\S/);
668
- const warningIndent = warningLines[1].search(/\S/);
669
- const errorIndent = errorLines[1].search(/\S/);
670
-
671
- // All continuation lines should start at the same position
672
- assertEquals(
673
- infoIndent,
674
- warningIndent,
675
- "Info and warning should have same indentation",
676
- );
677
- assertEquals(
678
- warningIndent,
679
- errorIndent,
680
- "Warning and error should have same indentation",
681
- );
682
- }
683
- });
684
-
685
- test("getPrettyFormatter() with automatic width detection", () => {
686
- const formatter = getPrettyFormatter({
687
- wordWrap: true, // Auto-detect width
688
- colors: false,
689
- });
690
-
691
- const longMessage =
692
- "This is a long message that should wrap at the detected terminal width";
693
- const record = createLogRecord("info", ["test"], [longMessage]);
694
- const result = formatter(record);
695
-
696
- // Should have wrapped at some reasonable width
697
- const lines = result.split("\n").filter((line) => line.length > 0);
698
- assert(lines.length >= 1, "Should have at least one line");
699
-
700
- // If wrapping occurred, continuation lines should be properly indented
701
- if (lines.length > 1) {
702
- const firstLine = lines[0];
703
- const continuationLine = lines[1];
704
-
705
- assert(firstLine.includes("✨"), "First line should contain icon");
706
- assert(
707
- continuationLine.startsWith(" "),
708
- "Continuation line should be indented",
709
- );
710
- }
711
- });
712
-
713
- test("getPrettyFormatter() with multiline interpolated values", () => {
714
- const formatter = getPrettyFormatter({
715
- wordWrap: 60,
716
- colors: false,
717
- align: true,
718
- });
719
-
720
- // Create an error that will have multiline output
721
- const error = new Error("Test error message");
722
- const record = createLogRecord("error", ["test"], [
723
- "Exception occurred: ",
724
- error,
725
- ]);
726
- const result = formatter(record);
727
-
728
- const lines = result.split("\n").filter((line) => line.length > 0);
729
-
730
- // Should have multiple lines due to error stack trace
731
- assert(
732
- lines.length >= 2,
733
- "Should have multiple lines for error with stack trace",
734
- );
735
-
736
- // First line should contain our message and start of error
737
- assert(
738
- lines[0].includes("Exception occurred:"),
739
- "First line should contain our message",
740
- );
741
- assert(lines[0].includes("Error:"), "First line should contain error start");
742
-
743
- // Error message might be on first or second line depending on wrapping
744
- const fullOutput = result;
745
- assert(
746
- fullOutput.includes("Test error message"),
747
- "Output should contain error message",
748
- );
749
-
750
- // Check that continuation lines are properly indented (should start with significant whitespace)
751
- for (let i = 1; i < lines.length; i++) {
752
- const line = lines[i];
753
- const trimmedLine = line.trimStart();
754
- const indentLength = line.length - trimmedLine.length;
755
- assert(
756
- indentLength >= 10,
757
- `Line ${i} should be indented (has ${indentLength} spaces)`,
758
- );
759
- }
760
-
761
- // Should contain stack trace somewhere
762
- const stackTraceLine = lines.find((line) => line.trim().startsWith("at "));
763
- assert(stackTraceLine, "Should contain a stack trace line");
764
- const trimmedStackTrace = stackTraceLine.trimStart();
765
- const stackIndentLength = stackTraceLine.length - trimmedStackTrace.length;
766
- assert(stackIndentLength >= 10, "Stack trace should be properly indented");
767
- });
768
-
769
- test("getPrettyFormatter() with multiline interpolated values (no align)", () => {
770
- const formatter = getPrettyFormatter({
771
- wordWrap: 50,
772
- colors: false,
773
- align: false,
774
- });
775
-
776
- const error = new Error("Test error");
777
- const record = createLogRecord("error", ["app"], [
778
- "Error: ",
779
- error,
780
- ]);
781
- const result = formatter(record);
782
-
783
- const lines = result.split("\n").filter((line) => line.length > 0);
784
-
785
- // Should have multiple lines
786
- assert(lines.length >= 2, "Should have multiple lines for error");
787
-
788
- // Check that stack trace lines are properly indented relative to the message start
789
- const firstLine = lines[0];
790
- assert(
791
- firstLine.includes("❌ error app Error:"),
792
- "First line should contain prefix and message start",
793
- );
794
-
795
- if (lines.length > 1) {
796
- const stackTraceLine = lines.find((line) => line.trim().startsWith("at "));
797
- if (stackTraceLine) {
798
- // Stack trace should be indented to align with message content
799
- assert(
800
- stackTraceLine.length > stackTraceLine.trimStart().length,
801
- "Stack trace line should be indented",
802
- );
803
- }
804
- }
805
- });
806
-
807
- test("properties set to true", () => {
808
- const formatter = getPrettyFormatter({
809
- properties: true,
810
- colors: false,
811
- inspectOptions: { colors: false },
812
- });
813
-
814
- const record = createLogRecord("info", ["test"], ["FooBar"], Date.now(), {
815
- foo: "bar",
816
- bar: "baz",
817
- });
818
- const result = formatter(record);
819
-
820
- // Should contain multiple lines due to wrapping
821
- const lines = result.split("\n");
822
- assertEquals(lines.length, 4); // Normal log line + formatted properties + newline
823
- assertEquals(
824
- lines[1].trim(),
825
- "Deno" in globalThis ? 'foo: "bar"' : "foo: 'bar'",
826
- );
827
- assertEquals(
828
- lines[2].trim(),
829
- "Deno" in globalThis ? 'bar: "baz"' : "bar: 'baz'",
830
- );
831
- });
832
-
833
- test("properties with long keys (regression test for #87)", () => {
834
- const formatter = getPrettyFormatter({
835
- properties: true,
836
- colors: false,
837
- inspectOptions: { colors: false },
838
- align: false, // Disable alignment for predictable output
839
- });
840
-
841
- // Use fixed timestamp for reproducible output
842
- const fixedTimestamp = new Date("2024-01-15T00:00:00Z").getTime();
843
-
844
- // Create properties with very long keys that will cause negative padding
845
- const longKeyProps: Record<string, unknown> = {
846
- VERY_LONG_PROPERTY_NAME_THAT_EXCEEDS_INDENT_WIDTH: "value1",
847
- ANOTHER_EXTREMELY_LONG_KEY_NAME_FOR_TESTING: "value2",
848
- SHORT: "value3",
849
- };
850
-
851
- const record = createLogRecord(
852
- "info",
853
- ["test"],
854
- ["Test message"],
855
- fixedTimestamp,
856
- longKeyProps,
857
- );
858
-
859
- // After the fix, this should not throw an error
860
- const result = formatter(record);
861
-
862
- // Check the exact output format
863
- // Note: Long keys have 0 padding, SHORT key still has some padding
864
- const expectedOutput = `✨ info test Test message
865
- VERY_LONG_PROPERTY_NAME_THAT_EXCEEDS_INDENT_WIDTH: ${
866
- "Deno" in globalThis ? '"value1"' : "'value1'"
867
- }
868
- ANOTHER_EXTREMELY_LONG_KEY_NAME_FOR_TESTING: ${
869
- "Deno" in globalThis ? '"value2"' : "'value2'"
870
- }
871
- SHORT: ${"Deno" in globalThis ? '"value3"' : "'value3'"}
872
- `;
873
-
874
- assertEquals(result, expectedOutput);
875
- });
876
-
877
- test("getPrettyFormatter() with getters option", () => {
878
- const formatter = getPrettyFormatter({
879
- colors: false,
880
- inspectOptions: { getters: true },
881
- });
882
-
883
- const recordWithGetters = createLogRecord("info", ["test"], [
884
- "Object with getter: ",
885
- {
886
- get computed() {
887
- return "getter result";
888
- },
889
- },
890
- ]);
891
-
892
- const result = formatter(recordWithGetters);
893
-
894
- // Should complete without errors and output should be present
895
- assertStringIncludes(result, "Object with getter:");
896
- });
897
-
898
- test("getPrettyFormatter() with showProxy option", () => {
899
- const formatter = getPrettyFormatter({
900
- colors: false,
901
- inspectOptions: { showProxy: true },
902
- });
903
-
904
- const proxyObject = new Proxy(
905
- { name: "original" },
906
- {
907
- get(target, prop) {
908
- return target[prop as keyof typeof target];
909
- },
910
- },
911
- );
912
-
913
- const record = createLogRecord("info", ["test"], [
914
- "Proxy object: ",
915
- proxyObject,
916
- ]);
917
-
918
- const result = formatter(record);
919
-
920
- // Should complete without errors and output should be present
921
- assertStringIncludes(result, "Proxy object:");
922
- assertStringIncludes(result, "original");
923
- });
924
-
925
- test("getPrettyFormatter() with both getters and showProxy options", () => {
926
- const formatter = getPrettyFormatter({
927
- colors: false,
928
- inspectOptions: { getters: true, showProxy: true },
929
- });
930
-
931
- const proxyWithGetters = new Proxy(
932
- {
933
- value: 42,
934
- get computed() {
935
- return this.value * 2;
936
- },
937
- },
938
- {
939
- get(target, prop) {
940
- return target[prop as keyof typeof target];
941
- },
942
- },
943
- );
944
-
945
- const record = createLogRecord("info", ["test"], [
946
- "Complex object: ",
947
- proxyWithGetters,
948
- ]);
949
-
950
- const result = formatter(record);
951
-
952
- // Should complete without errors and output should be present
953
- assertStringIncludes(result, "Complex object:");
954
- });