Skip to content

Commit

Permalink
Merge pull request #544 from gemini-testing/TESTPLANE-8
Browse files Browse the repository at this point in the history
feat: add error snippets support
  • Loading branch information
KuznetsovRoman authored May 1, 2024
2 parents 114d4d8 + c1020c4 commit fb63866
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 18 deletions.
107 changes: 105 additions & 2 deletions lib/common-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,12 @@ export const hasUnrelatedToScreenshotsErrors = (error: TestError): boolean => {
!isAssertViewError(error);
};

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

return pick(error, ['name', 'message', 'stack', 'stateName']);
return pick(error, ['name', 'message', 'stack', 'stateName', 'snippet']);
};

export const hasDiff = (assertViewResults: {name?: string}[]): boolean => {
Expand Down Expand Up @@ -258,3 +258,106 @@ export const isImageBufferData = (imageData: ImageBuffer | ImageFile | ImageBase
export const isImageInfoWithState = (imageInfo: ImageInfoFull): imageInfo is ImageInfoWithState => {
return Boolean((imageInfo as ImageInfoWithState).stateName);
};

export const trimArray = <T>(array: Array<T>): Array<T> => {
let indexBegin = 0;
let indexEnd = array.length;

while (indexBegin < array.length && !array[indexBegin]) {
indexBegin++;
}

while (indexEnd > 0 && !array[indexEnd - 1]) {
indexEnd--;
}

return array.slice(indexBegin, indexEnd);
};

const getErrorTitle = (e: Error): string => {
let errorName = e.name;

if (!errorName && e.stack) {
const columnIndex = e.stack.indexOf(':');

if (columnIndex !== -1) {
errorName = e.stack.slice(0, columnIndex);
} else {
errorName = e.stack.slice(0, e.stack.indexOf('\n'));
}
}

if (!errorName) {
errorName = 'Error';
}

return e.message ? `${errorName}: ${e.message}` : errorName;
};

const getErrorRawStackFrames = (e: Error & { stack: string }): string => {
const errorTitle = getErrorTitle(e) + '\n';
const errorTitleStackIndex = e.stack.indexOf(errorTitle);

if (errorTitleStackIndex !== -1) {
return e.stack.slice(errorTitleStackIndex + errorTitle.length);
}

const errorString = e.toString ? e.toString() + '\n' : '';
const errorStringIndex = e.stack.indexOf(errorString);

if (errorString && errorStringIndex !== -1) {
return e.stack.slice(errorStringIndex + errorString.length);
}

const errorMessageStackIndex = e.stack.indexOf(e.message);
const errorMessageEndsStackIndex = e.stack.indexOf('\n', errorMessageStackIndex + e.message.length);

return e.stack.slice(errorMessageEndsStackIndex + 1);
};

const cloneError = <T extends Error>(error: T): T => {
const originalProperties = ['name', 'message', 'stack'] as Array<keyof Error>;
const clonedError = new Error(error.message) as T;

originalProperties.forEach(property => {
delete clonedError[property];
});

const customProperties = Object.getOwnPropertyNames(error) as Array<keyof Error>;

originalProperties.concat(customProperties).forEach((property) => {
clonedError[property] = error[property] as any; // eslint-disable-line @typescript-eslint/no-explicit-any
});

return clonedError;
};

export const mergeSnippetIntoErrorStack = <T extends Error>(error: T & { snippet?: string }): T => {
if (!error.snippet) {
return error;
}

const clonedError = cloneError(error);

delete clonedError.snippet;

if (!error.stack) {
clonedError.stack = [
getErrorTitle(error),
error.snippet
].join('\n');

return clonedError;
}

const grayBegin = '\x1B[90m';
const grayEnd = '\x1B[39m';

clonedError.stack = [
getErrorTitle(error),
error.snippet,
grayBegin + getErrorRawStackFrames(error as Error & { stack: string }) + grayEnd
].join('\n');

return clonedError;
};
30 changes: 22 additions & 8 deletions lib/static/components/details.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export default class Details extends Component {
title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]).isRequired,
content: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.element, PropTypes.array]).isRequired,
extendClassNames: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
onClick: PropTypes.func
onClick: PropTypes.func,
asHtml: PropTypes.bool,
};

state = {isOpened: false};
Expand All @@ -27,6 +28,25 @@ export default class Details extends Component {
});
};

_getContent() {
const content = this.props.content;

return isFunction(content) ? content() : content
}

_renderContent() {
if (!this.state.isOpened) {
return null;
}

const children = this.props.asHtml ? null : this._getContent();
const extraProps = this.props.asHtml ? {dangerouslySetInnerHTML: {__html: this._getContent()}} : {};

return <div className='details__content' {...extraProps}>
{children}
</div>
}

render() {
const {title, content, extendClassNames} = this.props;
const className = classNames(
Expand All @@ -44,13 +64,7 @@ export default class Details extends Component {
<summary className='details__summary' onClick={this.handleClick}>
{title}
</summary>
{
this.state.isOpened
? <div className='details__content'>
{isFunction(content) ? content() : content}
</div>
: null
}
{this._renderContent()}
</details>
)
);
Expand Down
31 changes: 29 additions & 2 deletions lib/static/components/state/state-error.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,22 @@ import {bindActionCreators} from 'redux';
import PropTypes from 'prop-types';
import {isEmpty, map, isFunction} from 'lodash';
import ReactHtmlParser from 'react-html-parser';
import escapeHtml from "escape-html";
import ansiHtml from "ansi-html-community";
import * as actions from '../../modules/actions';
import ResizedScreenshot from './screenshot/resized';
import ErrorDetails from './error-details';
import Details from '../details';
import {ERROR_TITLE_TEXT_LENGTH} from '../../../constants/errors';
import {isAssertViewError, isImageDiffError, isNoRefImageError} from '../../../common-utils';
import {isAssertViewError, isImageDiffError, isNoRefImageError, mergeSnippetIntoErrorStack, trimArray} from '../../../common-utils';

ansiHtml.setColors({
reset: ["#", "#"],
cyan: "ff6188",
yellow: "5cb008",
magenta: "8e81cd",
green: "aa8720",
})

class StateError extends Component {
static propTypes = {
Expand Down Expand Up @@ -46,6 +56,10 @@ class StateError extends Component {
return null;
}

_wrapInPreformatted = (html) => {
return html ? `<pre>${html}</pre>` : html;
}

_errorToElements(error) {
return map(error, (value, key) => {
if (!value) {
Expand All @@ -58,6 +72,8 @@ class StateError extends Component {
if (typeof value === 'string') {
if (value.match(/\n/)) {
[titleText, ...content] = value.split('\n');

content = trimArray(content);
} else if (value.length < ERROR_TITLE_TEXT_LENGTH) {
titleText = value;
} else {
Expand All @@ -67,15 +83,19 @@ class StateError extends Component {
if (Array.isArray(content)) {
content = content.join('\n');
}

content = this._wrapInPreformatted(ansiHtml(escapeHtml(content)));
} else {
titleText = <span>show more</span>;
content = isFunction(value) ? value : () => value;
}

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

return <Details
key={key}
asHtml={asHtml}
title={title}
content={content}
extendClassNames="error__item"
Expand All @@ -97,7 +117,14 @@ class StateError extends Component {

return (
<div className="image-box__image image-box__image_single">
{this._shouldDrawErrorInfo(extendedError) && <div className="error">{this._errorToElements(extendedError)}</div>}
{
this._shouldDrawErrorInfo(extendedError)
? <div
className="error"
>
{this._errorToElements(mergeSnippetIntoErrorStack(extendedError))}
</div> : null
}
{errorDetails && <ErrorDetails errorDetails={errorDetails} />}
{this._drawImage()}
</div>
Expand Down
4 changes: 4 additions & 0 deletions lib/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,10 @@ a:active {
margin-bottom: 0;
}

.details__content pre {
margin: 5px 0;
}

.details_type_text .details__content {
margin: 5px 0;
background-color: #f0f2f5;
Expand Down
1 change: 1 addition & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface DiffOptions extends LooksSameOptions {
export interface TestError {
name: string;
message: string;
snippet?: string; // defined if testplane >= 8.11.0
stack?: string;
stateName?: string;
details?: ErrorDetails
Expand Down
23 changes: 18 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,14 @@
"dependencies": {
"@babel/runtime": "^7.22.5",
"@gemini-testing/sql.js": "^2.0.0",
"ansi-html-community": "^0.0.8",
"axios": "1.6.3",
"better-sqlite3": "^8.5.0",
"bluebird": "^3.5.3",
"body-parser": "^1.18.2",
"chalk": "^4.1.2",
"debug": "^4.1.1",
"escape-html": "^1.0.3",
"eventemitter2": "6.4.7",
"express": "^4.16.2",
"fast-glob": "^3.2.12",
Expand Down Expand Up @@ -115,6 +117,7 @@
"@types/chai": "^4.3.5",
"@types/debug": "^4.1.8",
"@types/enzyme": "^3.10.13",
"@types/escape-html": "^1.0.4",
"@types/express": "4.16",
"@types/fs-extra": "^7.0.0",
"@types/http-codes": "^1.0.4",
Expand Down
Loading

0 comments on commit fb63866

Please sign in to comment.