Skip to content

Commit

Permalink
Merge pull request #213 from gemini-testing/group-by-error
Browse files Browse the repository at this point in the history
feat: add grouping by error type
  • Loading branch information
CatWithApple committed Apr 2, 2019
2 parents c9fe3c5 + 7a566a3 commit b12903a
Show file tree
Hide file tree
Showing 24 changed files with 980 additions and 197 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ directory.
* **baseHost** (optional) - `String` - it changes original host for view in the browser; by default original host does not change
* **scaleImages** (optional) – `Boolean` – fit images into page width; `false` by default
* **lazyLoadOffset** (optional) - `Number` - allows you to specify how far above and below the viewport you want to begin loading images. Lazy loading would be disabled if you specify 0. `800` by default.
* **errorPatterns** (optional) - `Array` - error message patterns for 'Group by error' mode.
Array element must be `Object` ({'*name*': `String`, '*pattern*': `String`}) or `String` (interpret as *name* and *pattern*).
Test will be associated with group if test error matches on group error pattern.
New group will be created if test cannot be associated with existing groups.

Also there is ability to override plugin parameters by CLI options or environment variables
(see [configparser](https://github.com/gemini-testing/configparser)).
Expand All @@ -48,7 +52,14 @@ module.exports = {
enabled: true,
path: 'my/gemini-reports',
defaultView: 'all',
baseHost: 'test.com'
baseHost: 'test.com',
errorPatterns: [
'Parameter .* must be a string',
{
name: 'Cannot read property of undefined',
pattern: 'Cannot read property .* of undefined'
}
]
}
}
},
Expand All @@ -69,7 +80,14 @@ module.exports = {
enabled: true,
path: 'my/hermione-reports',
defaultView: 'all',
baseHost: 'test.com'
baseHost: 'test.com',
errorPatterns: [
'Parameter .* must be a string',
{
name: 'Cannot read property of undefined',
pattern: 'Cannot read property .* of undefined'
}
]
}
},
//...
Expand Down
36 changes: 36 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,36 @@ const assertString = (name) => assertType(name, _.isString, 'string');
const assertBoolean = (name) => assertType(name, _.isBoolean, 'boolean');
const assertNumber = (name) => assertType(name, _.isNumber, 'number');

const assertErrorPatterns = (errorPatterns) => {
if (!_.isArray(errorPatterns)) {
throw new Error(`"errorPatterns" option must be array, but got ${typeof errorPatterns}`);
}
for (const patternInfo of errorPatterns) {
if (!_.isString(patternInfo) && !_.isPlainObject(patternInfo)) {
throw new Error(`Element of "errorPatterns" option must be plain object or string, but got ${typeof patternInfo}`);
}
if (_.isPlainObject(patternInfo)) {
for (const field of ['name', 'pattern']) {
if (!_.isString(patternInfo[field])) {
throw new Error(`Field "${field}" in element of "errorPatterns" option must be string, but got ${typeof patternInfo[field]}`);
}
}
}
}
};

const mapErrorPatterns = (errorPatterns) => {
return errorPatterns.map(patternInfo => {
if (typeof patternInfo === 'string') {
return {
name: patternInfo,
pattern: patternInfo
};
}
return patternInfo;
});
};

const getParser = () => {
return root(section({
enabled: option({
Expand Down Expand Up @@ -53,6 +83,12 @@ const getParser = () => {
defaultValue: configDefaults.lazyLoadOffset,
parseEnv: JSON.parse,
validate: assertNumber('lazyLoadOffset')
}),
errorPatterns: option({
defaultValue: configDefaults.errorPatterns,
parseEnv: JSON.parse,
validate: assertErrorPatterns,
map: mapErrorPatterns
})
}), {envPrefix: ENV_PREFIX, cliPrefix: CLI_PREFIX});
};
Expand Down
3 changes: 2 additions & 1 deletion lib/constants/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = {
defaultView: 'all',
baseHost: '',
scaleImages: false,
lazyLoadOffset: 800
lazyLoadOffset: 800,
errorPatterns: []
}
};
4 changes: 2 additions & 2 deletions lib/report-builder-factory/report-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,14 @@ module.exports = class ReportBuilder {
}

getResult() {
const {defaultView, baseHost, scaleImages, lazyLoadOffset} = this._pluginConfig;
const {defaultView, baseHost, scaleImages, lazyLoadOffset, errorPatterns} = this._pluginConfig;

this._sortTree();

return _.extend({
skips: _.uniq(this._skips, JSON.stringify),
suites: this._tree.children,
config: {defaultView, baseHost, scaleImages, lazyLoadOffset},
config: {defaultView, baseHost, scaleImages, lazyLoadOffset, errorPatterns},
extraItems: this._extraItems,
date: new Date().toString()
}, this._stats);
Expand Down
5 changes: 5 additions & 0 deletions lib/static/components/controls/common-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ class ControlButtons extends Component {
isActive={Boolean(view.lazyLoadOffset)}
handler={actions.toggleLazyLoad}
/>
<ControlButton
label="Group by error"
isActive={Boolean(view.groupByError)}
handler={actions.toggleGroupByError}
/>
<BaseHostInput/>
<MenuBar />
</Fragment>
Expand Down
4 changes: 2 additions & 2 deletions lib/static/components/controls/common-filters.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
'use strict';

import React, {Component} from 'react';
import FilterByNameInput from './filter-by-name-input';
import TestNameFilterInput from './test-name-filter-input';

class CommonFilters extends Component {
render() {
return (
<div className="control-filters">
<FilterByNameInput/>
<TestNameFilterInput/>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,30 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import _ from 'lodash';
import {debounce} from 'lodash';
import * as actions from '../../modules/actions';

class FilterByNameInput extends Component {
class TestNameFilterInput extends Component {
static propTypes = {
filterByName: PropTypes.string.isRequired,
testNameFilter: PropTypes.string.isRequired,
actions: PropTypes.object.isRequired
}

constructor(props) {
super(props);

this.state = {
filterByName: this.props.filterByName
testNameFilter: this.props.testNameFilter
};

this._onChange = (event) => {
this.setState({filterByName: event.target.value});
this.setState({testNameFilter: event.target.value});
this._debouncedUpdate();
};

this._debouncedUpdate = _.debounce(
this._debouncedUpdate = debounce(
() => {
this.props.actions.updateFilterByName(this.state.filterByName);
this.props.actions.updateTestNameFilter(this.state.testNameFilter);
},
500,
{
Expand All @@ -41,15 +41,15 @@ class FilterByNameInput extends Component {
<input
className="text-input"
size="100"
value={this.state.filterByName}
placeholder="filter by name"
value={this.state.testNameFilter}
placeholder="filter by test name"
onChange={this._onChange}
/>
);
}
}

export default connect(
(state) => ({filterByName: state.view.filterByName}),
(state) => ({testNameFilter: state.view.testNameFilter}),
(dispatch) => ({actions: bindActionCreators(actions, dispatch)})
)(FilterByNameInput);
)(TestNameFilterInput);
47 changes: 47 additions & 0 deletions lib/static/components/error-groups/item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import Suites from '../suites';

export default class ErrorGroupsItem extends Component {
state = {
collapsed: true
};

static propTypes = {
group: PropTypes.object.isRequired
}

_toggleState = () => {
this.setState({collapsed: !this.state.collapsed});
}

render() {
const {name, pattern, count, tests} = this.props.group;

const body = this.state.collapsed
? null
: <div className="error-group__body error-group__body_guided">
<Suites errorGroupTests={tests}/>
<hr/>
</div>;

const className = classNames(
'error-group',
{'error-group_collapsed': this.state.collapsed}
);

return (
<div className={className}>
<div className="error-group__title" onClick={this._toggleState} title={pattern}>
<div className="error-group__name">{name}</div>
<i>&nbsp;({count})</i>
</div>
{body}
</div>
);
}
}
29 changes: 29 additions & 0 deletions lib/static/components/error-groups/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

import React, {Component} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';

import ErrorGroupsItem from './item';

class ErrorGroupsList extends Component {
static propTypes = {
groupedErrors: PropTypes.array.isRequired
};

render() {
const {groupedErrors} = this.props;

return groupedErrors.length === 0
? <div>There is no test failure to be displayed.</div>
: (
<div className="groupedErrors">
{groupedErrors.map(group => {
return <ErrorGroupsItem key={group.name} group={group} />;
})}
</div>
);
}
}

export default connect(({groupedErrors}) => ({groupedErrors}))(ErrorGroupsList);
5 changes: 3 additions & 2 deletions lib/static/components/gui.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import React, {Component, Fragment} from 'react';
import {connect} from 'react-redux';

import {initial} from '../modules/actions';
import ControlButtons from './controls/gui-controls';
import SkippedList from './skipped-list';
import Suites from './suites';
import Loading from './loading';
import ModalContainer from '../containers/modal';
import MainTree from './main-tree';

class Gui extends Component {
componentDidMount() {
Expand All @@ -21,7 +22,7 @@ class Gui extends Component {
<Fragment>
<ControlButtons />
<SkippedList />
<Suites />
<MainTree />
<Loading active={loading.active} content={loading.content} />
<ModalContainer />
</Fragment>
Expand Down
62 changes: 62 additions & 0 deletions lib/static/components/main-tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict';

import React, {Component} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {bindActionCreators} from 'redux';

import ErrorGroupsList from './error-groups/list';
import Suites from './suites';
import clientEvents from '../../gui/constants/client-events';
import {suiteBegin, testBegin, testResult, testsEnd} from '../modules/actions';

class MainTree extends Component {
static propTypes = {
gui: PropTypes.bool,
groupByError: PropTypes.bool.isRequired
}

componentDidMount() {
this.props.gui && this._subscribeToEvents();
}

_subscribeToEvents() {
const {actions} = this.props;
const eventSource = new EventSource('/events');
eventSource.addEventListener(clientEvents.BEGIN_SUITE, (e) => {
const data = JSON.parse(e.data);
actions.suiteBegin(data);
});

eventSource.addEventListener(clientEvents.BEGIN_STATE, (e) => {
const data = JSON.parse(e.data);
actions.testBegin(data);
});

[clientEvents.TEST_RESULT, clientEvents.ERROR].forEach((eventName) => {
eventSource.addEventListener(eventName, (e) => {
const data = JSON.parse(e.data);
actions.testResult(data);
});
});

eventSource.addEventListener(clientEvents.END, () => {
this.props.actions.testsEnd();
});
}

render() {
const {groupByError} = this.props;

return groupByError
? <ErrorGroupsList/>
: <Suites/>;
}
}

const actions = {testBegin, suiteBegin, testResult, testsEnd};

export default connect(
({gui, view: {groupByError}}) => ({gui, groupByError}),
(dispatch) => ({actions: bindActionCreators(actions, dispatch)})
)(MainTree);
4 changes: 2 additions & 2 deletions lib/static/components/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, {Component, Fragment} from 'react';
import Summary from './summary';
import ControlButtons from './controls/report-controls';
import SkippedList from './skipped-list';
import Suites from './suites';
import MainTree from './main-tree';

export default class Report extends Component {
render() {
Expand All @@ -13,7 +13,7 @@ export default class Report extends Component {
<Summary/>
<ControlButtons/>
<SkippedList/>
<Suites/>
<MainTree/>
</Fragment>
);
}
Expand Down
Loading

0 comments on commit b12903a

Please sign in to comment.