Skip to content

Commit c1020c4

Browse files
feat: add error snippets support
1 parent 114d4d8 commit c1020c4

File tree

10 files changed

+333
-18
lines changed

10 files changed

+333
-18
lines changed

lib/common-utils.ts

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,12 @@ export const hasUnrelatedToScreenshotsErrors = (error: TestError): boolean => {
138138
!isAssertViewError(error);
139139
};
140140

141-
export const getError = (error?: TestError): undefined | Pick<TestError, 'name' | 'message' | 'stack' | 'stateName'> => {
141+
export const getError = (error?: TestError): undefined | Pick<TestError, 'name' | 'message' | 'stack' | 'stateName' | 'snippet'> => {
142142
if (!error) {
143143
return undefined;
144144
}
145145

146-
return pick(error, ['name', 'message', 'stack', 'stateName']);
146+
return pick(error, ['name', 'message', 'stack', 'stateName', 'snippet']);
147147
};
148148

149149
export const hasDiff = (assertViewResults: {name?: string}[]): boolean => {
@@ -258,3 +258,106 @@ export const isImageBufferData = (imageData: ImageBuffer | ImageFile | ImageBase
258258
export const isImageInfoWithState = (imageInfo: ImageInfoFull): imageInfo is ImageInfoWithState => {
259259
return Boolean((imageInfo as ImageInfoWithState).stateName);
260260
};
261+
262+
export const trimArray = <T>(array: Array<T>): Array<T> => {
263+
let indexBegin = 0;
264+
let indexEnd = array.length;
265+
266+
while (indexBegin < array.length && !array[indexBegin]) {
267+
indexBegin++;
268+
}
269+
270+
while (indexEnd > 0 && !array[indexEnd - 1]) {
271+
indexEnd--;
272+
}
273+
274+
return array.slice(indexBegin, indexEnd);
275+
};
276+
277+
const getErrorTitle = (e: Error): string => {
278+
let errorName = e.name;
279+
280+
if (!errorName && e.stack) {
281+
const columnIndex = e.stack.indexOf(':');
282+
283+
if (columnIndex !== -1) {
284+
errorName = e.stack.slice(0, columnIndex);
285+
} else {
286+
errorName = e.stack.slice(0, e.stack.indexOf('\n'));
287+
}
288+
}
289+
290+
if (!errorName) {
291+
errorName = 'Error';
292+
}
293+
294+
return e.message ? `${errorName}: ${e.message}` : errorName;
295+
};
296+
297+
const getErrorRawStackFrames = (e: Error & { stack: string }): string => {
298+
const errorTitle = getErrorTitle(e) + '\n';
299+
const errorTitleStackIndex = e.stack.indexOf(errorTitle);
300+
301+
if (errorTitleStackIndex !== -1) {
302+
return e.stack.slice(errorTitleStackIndex + errorTitle.length);
303+
}
304+
305+
const errorString = e.toString ? e.toString() + '\n' : '';
306+
const errorStringIndex = e.stack.indexOf(errorString);
307+
308+
if (errorString && errorStringIndex !== -1) {
309+
return e.stack.slice(errorStringIndex + errorString.length);
310+
}
311+
312+
const errorMessageStackIndex = e.stack.indexOf(e.message);
313+
const errorMessageEndsStackIndex = e.stack.indexOf('\n', errorMessageStackIndex + e.message.length);
314+
315+
return e.stack.slice(errorMessageEndsStackIndex + 1);
316+
};
317+
318+
const cloneError = <T extends Error>(error: T): T => {
319+
const originalProperties = ['name', 'message', 'stack'] as Array<keyof Error>;
320+
const clonedError = new Error(error.message) as T;
321+
322+
originalProperties.forEach(property => {
323+
delete clonedError[property];
324+
});
325+
326+
const customProperties = Object.getOwnPropertyNames(error) as Array<keyof Error>;
327+
328+
originalProperties.concat(customProperties).forEach((property) => {
329+
clonedError[property] = error[property] as any; // eslint-disable-line @typescript-eslint/no-explicit-any
330+
});
331+
332+
return clonedError;
333+
};
334+
335+
export const mergeSnippetIntoErrorStack = <T extends Error>(error: T & { snippet?: string }): T => {
336+
if (!error.snippet) {
337+
return error;
338+
}
339+
340+
const clonedError = cloneError(error);
341+
342+
delete clonedError.snippet;
343+
344+
if (!error.stack) {
345+
clonedError.stack = [
346+
getErrorTitle(error),
347+
error.snippet
348+
].join('\n');
349+
350+
return clonedError;
351+
}
352+
353+
const grayBegin = '\x1B[90m';
354+
const grayEnd = '\x1B[39m';
355+
356+
clonedError.stack = [
357+
getErrorTitle(error),
358+
error.snippet,
359+
grayBegin + getErrorRawStackFrames(error as Error & { stack: string }) + grayEnd
360+
].join('\n');
361+
362+
return clonedError;
363+
};

lib/static/components/details.jsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export default class Details extends Component {
1010
title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]).isRequired,
1111
content: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.element, PropTypes.array]).isRequired,
1212
extendClassNames: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
13-
onClick: PropTypes.func
13+
onClick: PropTypes.func,
14+
asHtml: PropTypes.bool,
1415
};
1516

1617
state = {isOpened: false};
@@ -27,6 +28,25 @@ export default class Details extends Component {
2728
});
2829
};
2930

31+
_getContent() {
32+
const content = this.props.content;
33+
34+
return isFunction(content) ? content() : content
35+
}
36+
37+
_renderContent() {
38+
if (!this.state.isOpened) {
39+
return null;
40+
}
41+
42+
const children = this.props.asHtml ? null : this._getContent();
43+
const extraProps = this.props.asHtml ? {dangerouslySetInnerHTML: {__html: this._getContent()}} : {};
44+
45+
return <div className='details__content' {...extraProps}>
46+
{children}
47+
</div>
48+
}
49+
3050
render() {
3151
const {title, content, extendClassNames} = this.props;
3252
const className = classNames(
@@ -44,13 +64,7 @@ export default class Details extends Component {
4464
<summary className='details__summary' onClick={this.handleClick}>
4565
{title}
4666
</summary>
47-
{
48-
this.state.isOpened
49-
? <div className='details__content'>
50-
{isFunction(content) ? content() : content}
51-
</div>
52-
: null
53-
}
67+
{this._renderContent()}
5468
</details>
5569
)
5670
);

lib/static/components/state/state-error.jsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,22 @@ import {bindActionCreators} from 'redux';
66
import PropTypes from 'prop-types';
77
import {isEmpty, map, isFunction} from 'lodash';
88
import ReactHtmlParser from 'react-html-parser';
9+
import escapeHtml from "escape-html";
10+
import ansiHtml from "ansi-html-community";
911
import * as actions from '../../modules/actions';
1012
import ResizedScreenshot from './screenshot/resized';
1113
import ErrorDetails from './error-details';
1214
import Details from '../details';
1315
import {ERROR_TITLE_TEXT_LENGTH} from '../../../constants/errors';
14-
import {isAssertViewError, isImageDiffError, isNoRefImageError} from '../../../common-utils';
16+
import {isAssertViewError, isImageDiffError, isNoRefImageError, mergeSnippetIntoErrorStack, trimArray} from '../../../common-utils';
17+
18+
ansiHtml.setColors({
19+
reset: ["#", "#"],
20+
cyan: "ff6188",
21+
yellow: "5cb008",
22+
magenta: "8e81cd",
23+
green: "aa8720",
24+
})
1525

1626
class StateError extends Component {
1727
static propTypes = {
@@ -46,6 +56,10 @@ class StateError extends Component {
4656
return null;
4757
}
4858

59+
_wrapInPreformatted = (html) => {
60+
return html ? `<pre>${html}</pre>` : html;
61+
}
62+
4963
_errorToElements(error) {
5064
return map(error, (value, key) => {
5165
if (!value) {
@@ -58,6 +72,8 @@ class StateError extends Component {
5872
if (typeof value === 'string') {
5973
if (value.match(/\n/)) {
6074
[titleText, ...content] = value.split('\n');
75+
76+
content = trimArray(content);
6177
} else if (value.length < ERROR_TITLE_TEXT_LENGTH) {
6278
titleText = value;
6379
} else {
@@ -67,15 +83,19 @@ class StateError extends Component {
6783
if (Array.isArray(content)) {
6884
content = content.join('\n');
6985
}
86+
87+
content = this._wrapInPreformatted(ansiHtml(escapeHtml(content)));
7088
} else {
7189
titleText = <span>show more</span>;
7290
content = isFunction(value) ? value : () => value;
7391
}
7492

7593
const title = <Fragment><span className="error__item-key">{key}: </span>{titleText}</Fragment>;
94+
const asHtml = typeof content === "string";
7695

7796
return <Details
7897
key={key}
98+
asHtml={asHtml}
7999
title={title}
80100
content={content}
81101
extendClassNames="error__item"
@@ -97,7 +117,14 @@ class StateError extends Component {
97117

98118
return (
99119
<div className="image-box__image image-box__image_single">
100-
{this._shouldDrawErrorInfo(extendedError) && <div className="error">{this._errorToElements(extendedError)}</div>}
120+
{
121+
this._shouldDrawErrorInfo(extendedError)
122+
? <div
123+
className="error"
124+
>
125+
{this._errorToElements(mergeSnippetIntoErrorStack(extendedError))}
126+
</div> : null
127+
}
101128
{errorDetails && <ErrorDetails errorDetails={errorDetails} />}
102129
{this._drawImage()}
103130
</div>

lib/static/styles.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,10 @@ a:active {
757757
margin-bottom: 0;
758758
}
759759

760+
.details__content pre {
761+
margin: 5px 0;
762+
}
763+
760764
.details_type_text .details__content {
761765
margin: 5px 0;
762766
background-color: #f0f2f5;

lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface DiffOptions extends LooksSameOptions {
5959
export interface TestError {
6060
name: string;
6161
message: string;
62+
snippet?: string; // defined if testplane >= 8.11.0
6263
stack?: string;
6364
stateName?: string;
6465
details?: ErrorDetails

package-lock.json

Lines changed: 18 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,14 @@
7373
"dependencies": {
7474
"@babel/runtime": "^7.22.5",
7575
"@gemini-testing/sql.js": "^2.0.0",
76+
"ansi-html-community": "^0.0.8",
7677
"axios": "1.6.3",
7778
"better-sqlite3": "^8.5.0",
7879
"bluebird": "^3.5.3",
7980
"body-parser": "^1.18.2",
8081
"chalk": "^4.1.2",
8182
"debug": "^4.1.1",
83+
"escape-html": "^1.0.3",
8284
"eventemitter2": "6.4.7",
8385
"express": "^4.16.2",
8486
"fast-glob": "^3.2.12",
@@ -115,6 +117,7 @@
115117
"@types/chai": "^4.3.5",
116118
"@types/debug": "^4.1.8",
117119
"@types/enzyme": "^3.10.13",
120+
"@types/escape-html": "^1.0.4",
118121
"@types/express": "4.16",
119122
"@types/fs-extra": "^7.0.0",
120123
"@types/http-codes": "^1.0.4",

0 commit comments

Comments
 (0)