Skip to content

Commit 487ac86

Browse files
committed
feat(utils): add option to truncate error messages to one-liner
1 parent ab2fe54 commit 487ac86

File tree

7 files changed

+110
-15
lines changed

7 files changed

+110
-15
lines changed

packages/plugin-eslint/src/lib/meta/transform.unit.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ Custom options:
161161
).toEqual<Audit>({
162162
slug: 'angular-eslint-template-mouse-events-have-key-events',
163163
title:
164-
'[Accessibility] Ensures that the mouse events `mouseout` and `mouseover` are accompanied by `focus` and `blur` events respectively. Coding for the keyboard is important for users with physical disabilities who cannot use a mouse, AT compatibility, and s...',
164+
'[Accessibility] Ensures that the mouse events `mouseout` and `mouseover` are accompanied by `focus` and `blur` events respectively. Coding for the keyboard is important for users with physical disabilities who cannot use a mouse, AT compatibility, and scr…',
165165
description:
166166
'ESLint rule **mouse-events-have-key-events**, from _@angular-eslint/template_ plugin.',
167167
docsUrl:

packages/plugin-lighthouse/src/lib/runner/details/item-value.unit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ describe('formatTableItemPropertyValue', () => {
300300
) as string;
301301

302302
expect(formattedStr.length).toBeLessThanOrEqual(200);
303-
expect(formattedStr.slice(-3)).toBe('...');
303+
expect(formattedStr.slice(-1)).toBe('');
304304
});
305305

306306
it('should format value based on itemValueFormat "numeric" as int', () => {
@@ -352,7 +352,7 @@ describe('formatTableItemPropertyValue', () => {
352352
) as string;
353353

354354
expect(formattedStr.length).toBeLessThanOrEqual(500);
355-
expect(formattedStr.slice(-3)).toBe('...');
355+
expect(formattedStr.slice(-1)).toBe('');
356356
});
357357

358358
it('should format value based on itemValueFormat "multi"', () => {

packages/utils/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ export {
6060
transformLines,
6161
truncateDescription,
6262
truncateIssueMessage,
63+
truncateMultilineText,
6364
truncateText,
6465
truncateTitle,
66+
UNICODE_ELLIPSIS,
6567
} from './lib/formatting.js';
6668
export {
6769
getCurrentBranchOrTag,

packages/utils/src/lib/errors.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
import { ZodError, z } from 'zod';
2+
import { UNICODE_ELLIPSIS, truncateMultilineText } from './formatting.js';
3+
4+
export function stringifyError(
5+
error: unknown,
6+
format?: { oneline: boolean },
7+
): string {
8+
const truncate = (text: string) =>
9+
format?.oneline ? truncateMultilineText(text) : text;
210

3-
export function stringifyError(error: unknown): string {
411
if (error instanceof ZodError) {
512
const formattedError = z.prettifyError(error);
613
if (formattedError.includes('\n')) {
14+
if (format?.oneline) {
15+
return `${error.name} [${UNICODE_ELLIPSIS}]`;
16+
}
717
return `${error.name}:\n${formattedError}\n`;
818
}
919
return `${error.name}: ${formattedError}`;
1020
}
21+
1122
if (error instanceof Error) {
1223
if (error.name === 'Error' || error.message.startsWith(error.name)) {
13-
return error.message;
24+
return truncate(error.message);
1425
}
15-
return `${error.name}: ${error.message}`;
26+
return truncate(`${error.name}: ${error.message}`);
1627
}
1728
if (typeof error === 'string') {
18-
return error;
29+
return truncate(error);
1930
}
2031
return JSON.stringify(error);
2132
}

packages/utils/src/lib/errors.unit.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ describe('stringifyError', () => {
2626
);
2727
});
2828

29+
it('should truncate multiline error messages if one-liner requested', () => {
30+
expect(
31+
stringifyError(
32+
new Error(
33+
'Failed to execute 2 out of 5 plugins:\n- ESLint\n- Lighthouse',
34+
),
35+
{ oneline: true },
36+
),
37+
).toBe('Failed to execute 2 out of 5 plugins: […]');
38+
});
39+
2940
it('should prettify ZodError instances spanning multiple lines', () => {
3041
const schema = z.object({
3142
name: z.string().min(1),
@@ -44,6 +55,17 @@ describe('stringifyError', () => {
4455
`);
4556
});
4657

58+
it('should omit multiline ZodError message if one-liner requested', () => {
59+
const schema = z.object({
60+
name: z.string().min(1),
61+
address: z.string(),
62+
dateOfBirth: z.iso.date().optional(),
63+
});
64+
const { error } = schema.safeParse({ name: '', dateOfBirth: '' });
65+
66+
expect(stringifyError(error, { oneline: true })).toBe('ZodError […]');
67+
});
68+
4769
it('should prettify ZodError instances on one line if possible', () => {
4870
const schema = z.enum(['json', 'md']);
4971
const { error } = schema.safeParse('html');
@@ -73,4 +95,21 @@ describe('stringifyError', () => {
7395
→ at dateOfBirth
7496
`);
7597
});
98+
99+
it('should truncate SchemaValidationError if one-liner requested', () => {
100+
const schema = z
101+
.object({
102+
name: z.string().min(1),
103+
address: z.string(),
104+
dateOfBirth: z.iso.date().optional(),
105+
})
106+
.meta({ title: 'User' });
107+
const { error } = schema.safeParse({ name: '', dateOfBirth: '' });
108+
109+
expect(
110+
stringifyError(new SchemaValidationError(error!, schema, {}), {
111+
oneline: true,
112+
}),
113+
).toBe(`SchemaValidationError: Invalid ${ansis.bold('User')} […]`);
114+
});
76115
});

packages/utils/src/lib/formatting.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
MAX_TITLE_LENGTH,
55
} from '@code-pushup/models';
66

7+
export const UNICODE_ELLIPSIS = '…';
8+
79
export function roundDecimals(value: number, maxDecimals: number) {
810
const multiplier = Math.pow(10, maxDecimals);
911
return Math.round(value * multiplier) / multiplier;
@@ -87,7 +89,7 @@ export function truncateText(
8789
const {
8890
maxChars,
8991
position = 'end',
90-
ellipsis = '...',
92+
ellipsis = UNICODE_ELLIPSIS,
9193
} = typeof options === 'number' ? { maxChars: options } : options;
9294
if (text.length <= maxChars) {
9395
return text;
@@ -121,6 +123,27 @@ export function truncateIssueMessage(text: string): string {
121123
return truncateText(text, MAX_ISSUE_MESSAGE_LENGTH);
122124
}
123125

126+
export function truncateMultilineText(
127+
text: string,
128+
options?: { ellipsis?: string },
129+
): string {
130+
const { ellipsis = `[${UNICODE_ELLIPSIS}]` } = options ?? {};
131+
132+
const crlfIndex = text.indexOf('\r\n');
133+
const lfIndex = text.indexOf('\n');
134+
const index = crlfIndex >= 0 ? crlfIndex : lfIndex;
135+
136+
if (index < 0) {
137+
return text;
138+
}
139+
140+
const firstLine = text.slice(0, index);
141+
if (text.slice(index).trim().length === 0) {
142+
return firstLine;
143+
}
144+
return `${firstLine} ${ellipsis}`;
145+
}
146+
124147
export function transformLines(
125148
text: string,
126149
fn: (line: string) => string,

packages/utils/src/lib/formatting.unit.test.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
roundDecimals,
1111
slugify,
1212
transformLines,
13+
truncateMultilineText,
1314
truncateText,
1415
} from './formatting.js';
1516

@@ -135,7 +136,7 @@ describe('formatDate', () => {
135136
describe('truncateText', () => {
136137
it('should replace overflowing text with ellipsis at the end', () => {
137138
expect(truncateText('All work and no play makes Jack a dull boy', 32)).toBe(
138-
'All work and no play makes Ja...',
139+
'All work and no play makes Jack…',
139140
);
140141
});
141142

@@ -161,31 +162,50 @@ describe('truncateText', () => {
161162
maxChars: 10,
162163
position: 'start',
163164
}),
164-
).toBe('...dy day.');
165+
).toBe('…oudy day.');
165166
});
166167

167168
it('should produce truncated text with ellipsis at the middle', () => {
168169
expect(
169170
truncateText('Horrendous amounts of lint issues are present Tony!', {
170-
maxChars: 10,
171+
maxChars: 8,
171172
position: 'middle',
172173
}),
173-
).toBe('Hor...ny!');
174+
).toBe('Horny!');
174175
});
175176

176177
it('should produce truncated text with ellipsis at the end', () => {
177178
expect(truncateText("I'm Johnny!", { maxChars: 10, position: 'end' })).toBe(
178-
"I'm Joh...",
179+
"I'm Johnn…",
179180
);
180181
});
181182

182183
it('should produce truncated text with custom ellipsis', () => {
183-
expect(truncateText("I'm Johnny!", { maxChars: 10, ellipsis: '*' })).toBe(
184-
"I'm Johnn*",
184+
expect(truncateText("I'm Johnny!", { maxChars: 10, ellipsis: '...' })).toBe(
185+
"I'm Joh...",
185186
);
186187
});
187188
});
188189

190+
describe('transformMultilineText', () => {
191+
it('should replace additional lines with an ellipsis', () => {
192+
const error = `SchemaValidationError: Invalid CoreConfig in code-pushup.config.ts file
193+
✖ Invalid input: expected array, received undefined
194+
→ at plugins`;
195+
expect(truncateMultilineText(error)).toBe(
196+
'SchemaValidationError: Invalid CoreConfig in code-pushup.config.ts file […]',
197+
);
198+
});
199+
200+
it('should leave one-liner texts unchanged', () => {
201+
expect(truncateMultilineText('Hello, world!')).toBe('Hello, world!');
202+
});
203+
204+
it('should omit ellipsis if additional lines have no non-whitespace characters', () => {
205+
expect(truncateMultilineText('- item 1\n \n\n')).toBe('- item 1');
206+
});
207+
});
208+
189209
describe('transformLines', () => {
190210
it('should apply custom transformation to each line', () => {
191211
let count = 0;

0 commit comments

Comments
 (0)