diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3be38334f..50432c2f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,41 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+# [4.4.0](https://github.com/gemini-testing/html-reporter/compare/v4.3.0...v4.4.0) (2019-04-02)
+
+
+### Bug Fixes
+
+* **lazy-load:** recheck suite view on test filter ([1a125ec](https://github.com/gemini-testing/html-reporter/commit/1a125ec))
+
+
+### Features
+
+* add grouping by error type ([7a566a3](https://github.com/gemini-testing/html-reporter/commit/7a566a3))
+
+
+
+
+# [4.3.0](https://github.com/gemini-testing/html-reporter/compare/v4.2.0...v4.3.0) (2019-03-28)
+
+
+### Features
+
+* should not show date if it is empty ([c3da7a3](https://github.com/gemini-testing/html-reporter/commit/c3da7a3))
+
+
+
+
+# [4.2.0](https://github.com/gemini-testing/html-reporter/compare/v4.1.1...v4.2.0) (2019-03-28)
+
+
+### Features
+
+* add report creation date ([e84757e](https://github.com/gemini-testing/html-reporter/commit/e84757e))
+
+
+
## [4.1.1](https://github.com/gemini-testing/html-reporter/compare/v4.1.0...v4.1.1) (2019-03-25)
diff --git a/README.md b/README.md
index 993115ecf..2cae6075b 100644
--- a/README.md
+++ b/README.md
@@ -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)).
@@ -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'
+ }
+ ]
}
}
},
@@ -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'
+ }
+ ]
}
},
//...
diff --git a/lib/config.js b/lib/config.js
index 14295c7aa..2c8361964 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -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({
@@ -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});
};
diff --git a/lib/constants/defaults.js b/lib/constants/defaults.js
index f97bf5f15..9cea20add 100644
--- a/lib/constants/defaults.js
+++ b/lib/constants/defaults.js
@@ -6,6 +6,7 @@ module.exports = {
defaultView: 'all',
baseHost: '',
scaleImages: false,
- lazyLoadOffset: 800
+ lazyLoadOffset: 800,
+ errorPatterns: []
}
};
diff --git a/lib/report-builder-factory/report-builder.js b/lib/report-builder-factory/report-builder.js
index efff116c9..3f12f44cb 100644
--- a/lib/report-builder-factory/report-builder.js
+++ b/lib/report-builder-factory/report-builder.js
@@ -205,15 +205,16 @@ 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},
- extraItems: this._extraItems
+ config: {defaultView, baseHost, scaleImages, lazyLoadOffset, errorPatterns},
+ extraItems: this._extraItems,
+ date: new Date().toString()
}, this._stats);
}
diff --git a/lib/static/components/controls/common-controls.js b/lib/static/components/controls/common-controls.js
index 64f0ad55b..a93b14a6f 100644
--- a/lib/static/components/controls/common-controls.js
+++ b/lib/static/components/controls/common-controls.js
@@ -64,6 +64,11 @@ class ControlButtons extends Component {
isActive={Boolean(view.lazyLoadOffset)}
handler={actions.toggleLazyLoad}
/>
+
diff --git a/lib/static/components/controls/common-filters.js b/lib/static/components/controls/common-filters.js
index 81af713e8..401d2f87b 100644
--- a/lib/static/components/controls/common-filters.js
+++ b/lib/static/components/controls/common-filters.js
@@ -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 (
There is no test failure to be displayed.
+ : (
+
- {suiteIds.map((suiteId) => {
+ {visibleSuiteIds.map((suiteId) => {
const sectionProps = {
key: suiteId,
suite: suites[suiteId],
- filterByName: filterByName,
- filteredBrowsers: filteredBrowsers
+ testNameFilter,
+ filteredBrowsers,
+ errorGroupTests
};
if (lazyLoadOffset > 0) {
@@ -75,29 +49,16 @@ class Suites extends Component {
}
}
-const actions = {testBegin, suiteBegin, testResult, testsEnd};
-
export default connect(
(state) => {
- const {filterByName, filteredBrowsers, lazyLoadOffset} = state.view;
- let suiteIds = state.suiteIds[state.view.viewMode];
-
- if (filteredBrowsers.length > 0) {
- suiteIds = suiteIds.filter(id => shouldSuiteBeShownByBrowser(state.suites[id], filteredBrowsers));
- }
-
- if (filterByName) {
- suiteIds = suiteIds.filter(id => shouldSuiteBeShownByName(state.suites[id], filterByName));
- }
+ const {testNameFilter, filteredBrowsers, lazyLoadOffset} = state.view;
return ({
- suiteIds,
- suites: state.suites,
- gui: state.gui,
- filterByName,
+ suiteIds: state.suiteIds[state.view.viewMode],
+ testNameFilter,
filteredBrowsers,
- lazyLoadOffset
+ lazyLoadOffset,
+ suites: state.suites
});
- },
- (dispatch) => ({actions: bindActionCreators(actions, dispatch)})
+ }
)(Suites);
diff --git a/lib/static/components/summary/index.js b/lib/static/components/summary/index.js
index 20a3493c2..f77391ab2 100644
--- a/lib/static/components/summary/index.js
+++ b/lib/static/components/summary/index.js
@@ -14,12 +14,18 @@ class Summary extends Component {
failed: PropTypes.number.isRequired,
skipped: PropTypes.number.isRequired,
retries: PropTypes.number.isRequired
- })
+ }),
+ date: PropTypes.string
}
render() {
+ const {date} = this.props;
const {total, passed, failed, skipped, retries} = this.props.stats;
+ const dateBlock = date
+ ?
created at {date}
+ : null;
+
return (
@@ -27,6 +33,7 @@ class Summary extends Component {
+ {dateBlock}
);
}
@@ -34,11 +41,12 @@ class Summary extends Component {
export default connect(
(state) => {
- const {stats} = state;
+ const {stats, date} = state;
const {filteredBrowsers} = state.view;
const statsToShow = getStats(stats, filteredBrowsers);
return {
- stats: statsToShow
+ stats: statsToShow,
+ date
};
})(Summary);
diff --git a/lib/static/modules/action-names.js b/lib/static/modules/action-names.js
index b0bc82bb4..e0ece855d 100644
--- a/lib/static/modules/action-names.js
+++ b/lib/static/modules/action-names.js
@@ -28,6 +28,7 @@ export default {
VIEW_TOGGLE_ONLY_DIFF: 'VIEW_TOGGLE_ONLY_DIFF',
VIEW_UPDATE_BASE_HOST: 'VIEW_UPDATE_BASE_HOST',
VIEW_UPDATE_FILTER_BY_NAME: 'VIEW_UPDATE_FILTER_BY_NAME',
+ VIEW_TOGGLE_GROUP_BY_ERROR: 'VIEW_TOGGLE_GROUP_BY_ERROR',
VIEW_TOGGLE_SCALE_IMAGES: 'VIEW_TOGGLE_SCALE_IMAGES',
VIEW_TOGGLE_LAZY_LOAD_IMAGES: 'VIEW_TOGGLE_LAZY_LOAD_IMAGES'
};
diff --git a/lib/static/modules/actions.js b/lib/static/modules/actions.js
index 6c5681e2c..c4bddb677 100644
--- a/lib/static/modules/actions.js
+++ b/lib/static/modules/actions.js
@@ -98,14 +98,15 @@ export const collapseAll = () => triggerViewChanges({type: actionNames.VIEW_COLL
export const toggleSkipped = () => triggerViewChanges({type: actionNames.VIEW_TOGGLE_SKIPPED});
export const toggleOnlyDiff = () => triggerViewChanges({type: actionNames.VIEW_TOGGLE_ONLY_DIFF});
export const toggleScaleImages = () => triggerViewChanges({type: actionNames.VIEW_TOGGLE_SCALE_IMAGES});
+export const toggleGroupByError = () => ({type: actionNames.VIEW_TOGGLE_GROUP_BY_ERROR});
export const toggleLazyLoad = () => ({type: actionNames.VIEW_TOGGLE_LAZY_LOAD_IMAGES});
export const updateBaseHost = (host) => {
window.localStorage.setItem('_gemini-replace-host', host);
return {type: actionNames.VIEW_UPDATE_BASE_HOST, host};
};
-export const updateFilterByName = (filterByName) => {
- return {type: actionNames.VIEW_UPDATE_FILTER_BY_NAME, filterByName};
+export const updateTestNameFilter = (testNameFilter) => {
+ return triggerViewChanges({type: actionNames.VIEW_UPDATE_FILTER_BY_NAME, testNameFilter});
};
export function changeViewMode(mode) {
diff --git a/lib/static/modules/default-state.js b/lib/static/modules/default-state.js
index 880c50b6f..b8838244b 100644
--- a/lib/static/modules/default-state.js
+++ b/lib/static/modules/default-state.js
@@ -7,6 +7,7 @@ export default Object.assign(defaults, {
running: false,
autoRun: false,
skips: [],
+ groupedErrors: [],
suites: {},
suiteIds: {
all: [],
@@ -32,7 +33,8 @@ export default Object.assign(defaults, {
showOnlyDiff: false,
scaleImages: false,
baseHost: '',
- filterByName: '',
- filteredBrowsers: []
+ testNameFilter: '',
+ filteredBrowsers: [],
+ groupByError: false
}
});
diff --git a/lib/static/modules/group-errors.js b/lib/static/modules/group-errors.js
new file mode 100644
index 000000000..c1e8b4717
--- /dev/null
+++ b/lib/static/modules/group-errors.js
@@ -0,0 +1,136 @@
+'use strict';
+
+const {get} = require('lodash');
+const {isSuccessStatus} = require('../../common-utils');
+
+/**
+ * @param {object} suites
+ * @param {array} errorPatterns
+ * @param {array} filteredBrowsers
+ * @param {string} [testNameFilter]
+ * @return {array}
+ */
+function groupErrors({suites, errorPatterns = [], filteredBrowsers = [], testNameFilter = ''}) {
+ const testWithErrors = extractErrors(suites);
+
+ const errorGroupsList = getErrorGroupList(testWithErrors, errorPatterns, filteredBrowsers, testNameFilter);
+
+ errorGroupsList.sort((a, b) => {
+ const result = b.count - a.count;
+ if (result === 0) {
+ return a.name.localeCompare(b.name);
+ }
+ return result;
+ });
+
+ return errorGroupsList;
+}
+
+function extractErrors(rootSuites) {
+ const testWithErrors = {};
+
+ const extract = (suites) => {
+ for (const suite of Object.values(suites)) {
+ const testName = suite.suitePath.join(' ');
+ const browsersWithError = {};
+
+ if (suite.browsers) {
+ for (const browser of suite.browsers) {
+ if (isSuccessStatus(browser.result.status)) {
+ continue;
+ }
+ const retries = [...browser.retries, browser.result];
+ const errorsInBrowser = extractErrorsFromRetries(retries);
+ if (errorsInBrowser.length) {
+ browsersWithError[browser.name] = errorsInBrowser;
+ }
+ }
+ }
+ if (Object.keys(browsersWithError).length) {
+ testWithErrors[testName] = browsersWithError;
+ }
+ if (suite.children) {
+ extract(suite.children);
+ }
+ }
+ };
+
+ extract(rootSuites);
+
+ return testWithErrors;
+}
+
+function extractErrorsFromRetries(retries) {
+ const errorsInRetry = new Set();
+
+ for (const retry of retries) {
+ for (const {error} of [...retry.imagesInfo, retry]) {
+ if (get(error, 'message')) {
+ errorsInRetry.add(error.message);
+ }
+ }
+ }
+ return [...errorsInRetry];
+}
+
+function getErrorGroupList(testWithErrors, errorPatterns, filteredBrowsers, testNameFilter) {
+ const errorGroups = {};
+ const errorPatternsWithRegExp = addRegExpToErrorPatterns(errorPatterns);
+
+ for (const [testName, browsers] of Object.entries(testWithErrors)) {
+ if (testNameFilter && !testName.includes(testNameFilter)) {
+ continue;
+ }
+
+ for (const [browserName, errors] of Object.entries(browsers)) {
+ if (filteredBrowsers.length !== 0 && !filteredBrowsers.includes(browserName)) {
+ continue;
+ }
+ for (const errorText of errors) {
+ const patternInfo = matchGroup(errorText, errorPatternsWithRegExp);
+ const {pattern, name} = patternInfo;
+
+ if (!errorGroups.hasOwnProperty(name)) {
+ errorGroups[name] = {
+ pattern,
+ name,
+ tests: {},
+ count: 0
+ };
+ }
+ const group = errorGroups[name];
+ if (!group.tests.hasOwnProperty(testName)) {
+ group.tests[testName] = [];
+ }
+ if (!group.tests[testName].includes(browserName)) {
+ group.tests[testName].push(browserName);
+ group.count++;
+ }
+ }
+ }
+ }
+
+ return Object.values(errorGroups);
+}
+
+function addRegExpToErrorPatterns(errorPatterns) {
+ return errorPatterns.map(patternInfo => ({
+ ...patternInfo,
+ regexp: new RegExp(patternInfo.pattern)
+ }));
+}
+
+function matchGroup(errorText, errorPatternsWithRegExp) {
+ for (const group of errorPatternsWithRegExp) {
+ if (errorText.match(group.regexp)) {
+ return group;
+ }
+ }
+
+ return {
+ name: errorText,
+ pattern: errorText
+ };
+}
+
+module.exports = {groupErrors};
diff --git a/lib/static/modules/reducer.js b/lib/static/modules/reducer.js
index 512b2343d..b3355b68a 100644
--- a/lib/static/modules/reducer.js
+++ b/lib/static/modules/reducer.js
@@ -4,33 +4,38 @@ import url from 'url';
import actionNames from './action-names';
import defaultState from './default-state';
import {assign, merge, filter, map, clone, cloneDeep, reduce, find, last} from 'lodash';
-import {isSuiteFailed, setStatusToAll, findNode, setStatusForBranch} from './utils';
+import {isSuiteFailed, setStatusToAll, findNode, setStatusForBranch, dateToLocaleString} from './utils';
+import {groupErrors} from './group-errors';
const compiledData = window.data || defaultState;
const localStorage = window.localStorage;
-function getInitialState(compiledData) {
+function getInitialState(data) {
const {skips, suites, config, total, updated, passed,
- failed, skipped, warned, retries, perBrowser, extraItems, gui = false} = compiledData;
- const formattedSuites = formatSuitesData(suites);
+ failed, skipped, warned, retries, perBrowser, extraItems, gui = false, date} = data;
+ const {errorPatterns, scaleImages, lazyLoadOffset, defaultView} = config;
const parsedURL = new URL(window.location.href);
const filteredBrowsers = parsedURL.searchParams.getAll('browser');
+ const formattedSuites = formatSuitesData(suites);
+ const groupedErrors = groupErrors({suites: formattedSuites.suites, errorPatterns, filteredBrowsers});
+
return merge(defaultState, {
gui,
skips,
+ groupedErrors,
config,
extraItems,
+ date: dateToLocaleString(date),
stats: {
all: {total, updated, passed, failed, skipped, retries, warned},
perBrowser
},
view: {
- viewMode: config.defaultView,
- scaleImages: config.scaleImages,
- lazyLoadOffset: config.lazyLoadOffset,
+ viewMode: defaultView,
+ scaleImages,
+ lazyLoadOffset,
..._loadBaseHost(config.baseHost, localStorage),
- filterByName: '',
filteredBrowsers
}
}, formattedSuites);
@@ -39,21 +44,57 @@ function getInitialState(compiledData) {
export default function reducer(state = getInitialState(compiledData), action) {
switch (action.type) {
case actionNames.VIEW_INITIAL: {
- const {gui, autoRun, suites, skips, extraItems, config: {scaleImages, lazyLoadOffset}} = action.payload;
+ const {
+ gui,
+ autoRun,
+ suites,
+ skips,
+ extraItems,
+ config: {scaleImages, lazyLoadOffset}
+ } = action.payload;
+ const {errorPatterns} = state.config;
+ const {filteredBrowsers, testNameFilter} = state.view;
+
const formattedSuites = formatSuitesData(suites);
+ const groupedErrors = groupErrors({
+ suites: formattedSuites.suites,
+ errorPatterns,
+ filteredBrowsers,
+ testNameFilter
+ });
- return merge({}, state, {gui, autoRun, skips, extraItems, view: {scaleImages, lazyLoadOffset}}, formattedSuites);
+ return merge(
+ {},
+ state,
+ {
+ gui,
+ autoRun,
+ skips,
+ groupedErrors,
+ extraItems,
+ view: {scaleImages, lazyLoadOffset}
+ },
+ formattedSuites
+ );
}
case actionNames.RUN_ALL_TESTS: {
const suites = clone(state.suites);
setStatusToAll(suites, action.payload.status);
- return merge({}, state, {running: true, suites}); // TODO: rewrite store on run all tests
+ // TODO: rewrite store on run all tests
+ return merge({}, state, {running: true, suites, view: {groupByError: false}});
}
case actionNames.RUN_FAILED_TESTS:
case actionNames.RETRY_SUITE:
case actionNames.RETRY_TEST: {
- return assign(clone(state), {running: true});
+ return {
+ ...state,
+ running: true,
+ view: {
+ ...state.view,
+ groupByError: false
+ }
+ };
}
case actionNames.SUITE_BEGIN: {
const suites = clone(state.suites);
@@ -128,14 +169,31 @@ export default function reducer(state = getInitialState(compiledData), action) {
return _mutateStateView(state, {baseHost, parsedHost});
}
case actionNames.VIEW_UPDATE_FILTER_BY_NAME: {
- return _mutateStateView(state, {
- filterByName: action.filterByName
- });
+ const {testNameFilter} = action;
+ const {
+ suites,
+ config: {errorPatterns},
+ view: {filteredBrowsers}
+ } = state;
+
+ const groupedErrors = groupErrors({suites, errorPatterns, filteredBrowsers, testNameFilter});
+
+ return {
+ ...state,
+ groupedErrors,
+ view: {
+ ...state.view,
+ testNameFilter
+ }
+ };
}
case actionNames.CLOSE_SECTIONS: {
const closeIds = action.payload;
return assign(clone(state), {closeIds});
}
+ case actionNames.VIEW_TOGGLE_GROUP_BY_ERROR: {
+ return _mutateStateView(state, {groupByError: !state.view.groupByError});
+ }
case actionNames.TOGGLE_TEST_RESULT: {
const {opened} = action.payload;
return updateTestState(state, action, {opened});
@@ -164,6 +222,10 @@ export default function reducer(state = getInitialState(compiledData), action) {
}
function addTestResult(state, action) {
+ const {
+ config: {errorPatterns},
+ view: {filteredBrowsers, testNameFilter}
+ } = state;
const suites = clone(state.suites);
[].concat(action.payload).forEach((suite) => {
@@ -186,7 +248,9 @@ function addTestResult(state, action) {
const suiteIds = clone(state.suiteIds);
assign(suiteIds, {failed: getFailedSuiteIds(suites)});
- return assign({}, state, {suiteIds, suites});
+ const groupedErrors = groupErrors({suites, errorPatterns, filteredBrowsers, testNameFilter});
+
+ return assign({}, state, {suiteIds, suites, groupedErrors});
}
function updateTestState(state, action, testState) {
@@ -291,3 +355,4 @@ function forceUpdateSuiteData(suites, test) {
const id = getSuiteId(test);
suites[id] = cloneDeep(suites[id]);
}
+
diff --git a/lib/static/modules/utils.js b/lib/static/modules/utils.js
index 4d5b55e86..9c865927b 100644
--- a/lib/static/modules/utils.js
+++ b/lib/static/modules/utils.js
@@ -1,6 +1,6 @@
'use strict';
-const {forOwn, pick, isArray, find, get, values} = require('lodash');
+const {forOwn, pick, isArray, find, get, values, isEmpty} = require('lodash');
const {isFailStatus, isErroredStatus, isSkippedStatus, determineStatus} = require('../../common-utils');
const {getCommonErrors} = require('../../constants/errors');
@@ -112,27 +112,6 @@ function setStatusForBranch(nodes, suitePath) {
setStatusForBranch(nodes, suitePath.slice(0, -1));
}
-function shouldSuiteBeShownByName(suite, filterByName) {
- const suiteFullPath = suite.suitePath.join(' ');
-
- if (suiteFullPath.includes(filterByName)) {
- return true;
- }
- if (suite.hasOwnProperty('children')) {
- return suite.children.some(child => shouldSuiteBeShownByName(child, filterByName));
- }
-
- return false;
-}
-
-function shouldSuiteBeShownByBrowser(suite, filteredBrowsers) {
- if (suite.hasOwnProperty('browsers')) {
- return suite.browsers.some(browser => filteredBrowsers.includes(browser.name));
- }
-
- return suite.children.some(child => shouldSuiteBeShownByBrowser(child, filteredBrowsers));
-}
-
function getStats(stats, filteredBrowsers) {
if (filteredBrowsers.length === 0 || !stats.perBrowser) {
return stats.all;
@@ -154,6 +133,49 @@ function getStats(stats, filteredBrowsers) {
return resStats;
}
+function dateToLocaleString(date) {
+ if (!date) {
+ return '';
+ }
+ const lang = isEmpty(navigator.languages) ? navigator.language : navigator.languages[0];
+ return new Date(date).toLocaleString(lang);
+}
+
+function shouldSuiteBeShown({suite, testNameFilter = '', filteredBrowsers = [], errorGroupTests = {}}) {
+ const strictTestNameFilters = Object.keys(errorGroupTests);
+
+ if (suite.hasOwnProperty('children')) {
+ return suite.children.some(child => shouldSuiteBeShown({suite: child, testNameFilter, errorGroupTests, filteredBrowsers}));
+ }
+
+ const suiteFullPath = suite.suitePath.join(' ');
+
+ const matchName = !testNameFilter || suiteFullPath.includes(testNameFilter);
+
+ const strictMatchNames = strictTestNameFilters.length === 0
+ || strictTestNameFilters.includes(suiteFullPath);
+
+ const matchBrowsers = filteredBrowsers.length === 0
+ || !suite.hasOwnProperty('browsers')
+ || suite.browsers.some(({name}) => filteredBrowsers.includes(name));
+
+ return matchName && strictMatchNames && matchBrowsers;
+}
+
+function shouldBrowserBeShown({browser, fullTestName, filteredBrowsers = [], errorGroupTests = {}}) {
+ const {name} = browser;
+ let errorGroupBrowsers = [];
+
+ if (errorGroupTests && errorGroupTests.hasOwnProperty(fullTestName)) {
+ errorGroupBrowsers = errorGroupTests[fullTestName];
+ }
+
+ const matchFilteredBrowsers = filteredBrowsers.length === 0 || filteredBrowsers.includes(name);
+ const matchErrorGroupBrowsers = errorGroupBrowsers.length === 0 || errorGroupBrowsers.includes(name);
+
+ return matchFilteredBrowsers && matchErrorGroupBrowsers;
+}
+
module.exports = {
hasNoRefImageErrors,
hasFails,
@@ -164,7 +186,8 @@ module.exports = {
findNode,
setStatusToAll,
setStatusForBranch,
- shouldSuiteBeShownByName,
- shouldSuiteBeShownByBrowser,
- getStats
+ getStats,
+ dateToLocaleString,
+ shouldSuiteBeShown,
+ shouldBrowserBeShown
};
diff --git a/lib/static/styles.css b/lib/static/styles.css
index 1a4cd1fff..d17c367c8 100644
--- a/lib/static/styles.css
+++ b/lib/static/styles.css
@@ -148,14 +148,24 @@
background-size: contain;
background-repeat: no-repeat;
}
+.error-group__title {
+ display: flex;
+
+ user-select: none;
+}
+
+.error-group__name {
+ cursor: pointer;
+ font-weight: bold;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
.section__title {
font-weight: bold;
cursor: pointer;
- -moz-user-select: none;
- -webkit-user-select: none;
- -ms-user-select: none;
user-select: none;
}
@@ -163,18 +173,17 @@
color: #ccc;
cursor: default;
- -moz-user-select: text;
- -webkit-user-select: text;
- -ms-user-select: text;
user-select: text;
}
.section__title:before,
+.error-group__title:before,
.state-title:before {
height: 18px;
}
.section__title:before,
+.error-group__title:before,
.state-title:before,
.toggle-open__switcher:before {
display: inline-block;
@@ -185,6 +194,7 @@
}
.section .section__title:hover,
+.error-group__name:hover,
.state-title:hover {
color: #2d3e50;
}
@@ -210,11 +220,13 @@
color: #ccc;
}
-.section__body {
+.section__body,
+.error-group__body {
padding-left: 15px;
}
-.section__body_guided {
+.section__body_guided,
+.error-group__body_guided {
border-left: 1px dotted #ccc;
}
@@ -261,6 +273,7 @@
}
.section_collapsed .section__title:before,
+.error-group_collapsed .error-group__title:before,
.state-title_collapsed:before,
.toggle-open_collapsed .toggle-open__switcher:before {
-webkit-transform: rotate(-90deg);
@@ -305,9 +318,6 @@
font-weight: bold;
cursor: pointer;
- -moz-user-select: none;
- -webkit-user-select: none;
- -ms-user-select: none;
user-select: none;
}
@@ -589,3 +599,8 @@ a:active {
opacity: 0.3;
background-color: #FF00FF;
}
+
+.summary__date {
+ float: right;
+ color: gray;
+}
diff --git a/package-lock.json b/package-lock.json
index cf36cfa73..c80fee907 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "html-reporter",
- "version": "4.1.1",
+ "version": "4.4.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index 84670191a..4638a5403 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "html-reporter",
- "version": "4.1.1",
+ "version": "4.4.0",
"description": "Plugin for gemini and hermione which is intended to aggregate the results of tests running into html report",
"scripts": {
"lint": "eslint .",
diff --git a/test/lib/static/components/suites.js b/test/lib/static/components/suites.js
index e1487d165..61577217d 100644
--- a/test/lib/static/components/suites.js
+++ b/test/lib/static/components/suites.js
@@ -4,6 +4,7 @@ import proxyquire from 'proxyquire';
import {defaultsDeep} from 'lodash';
import {mkConnectedComponent} from './utils';
+import {mkState} from '../../../utils';
import {config} from 'lib/constants/defaults';
import clientEvents from 'lib/constants/client-events';
@@ -19,6 +20,9 @@ describe('
', () => {
initialState = defaultsDeep(initialState, {
gui: false,
suiteIds: {all: ['suite1']},
+ suites: {'suite1': mkState({
+ suitePath: ['suite1']
+ })},
view: {viewMode: 'all', filteredBrowsers: [], lazyLoadOffset: config.lazyLoadOffset}
});
diff --git a/test/lib/static/modules/group-errors.js b/test/lib/static/modules/group-errors.js
new file mode 100644
index 000000000..c62aab152
--- /dev/null
+++ b/test/lib/static/modules/group-errors.js
@@ -0,0 +1,343 @@
+'use strict';
+
+const {groupErrors} = require('../../../../lib/static/modules/group-errors');
+const {
+ mkSuite,
+ mkState,
+ mkBrowserResult,
+ mkSuiteTree,
+ mkTestResult
+} = require('../../../utils');
+
+describe('static/modules/group-errors', () => {
+ it('should not collect errors from success test', () => {
+ const suites = [
+ mkSuiteTree({
+ browsers: [
+ mkBrowserResult({
+ result: mkTestResult({
+ status: 'success'
+ }),
+ retries: [
+ mkTestResult({
+ error: {
+ message: 'message stub'
+ }
+ })
+ ]
+ })
+ ]
+ })
+ ];
+
+ const result = groupErrors({suites});
+
+ assert.deepEqual(result, []);
+ });
+
+ it('should collect errors from error and imagesInfo[].error', () => {
+ const suites = [
+ mkSuiteTree({
+ browsers: [
+ mkBrowserResult({
+ result: mkTestResult({
+ error: {
+ message: 'message stub first'
+ },
+ imagesInfo: [
+ {error: {message: 'message stub second'}}
+ ]
+ })
+ })
+ ]
+ })
+ ];
+
+ const result = groupErrors({suites});
+
+ assert.deepEqual(result, [
+ {
+ count: 1,
+ name: 'message stub first',
+ pattern: 'message stub first',
+ tests: {
+ 'default-suite default-state': ['default-bro']
+ }
+ },
+ {
+ count: 1,
+ name: 'message stub second',
+ pattern: 'message stub second',
+ tests: {
+ 'default-suite default-state': ['default-bro']
+ }
+ }
+ ]);
+ });
+
+ it('should collect errors from result and retries', () => {
+ const suites = [
+ mkSuiteTree({
+ browsers: [
+ mkBrowserResult({
+ result: mkTestResult({
+ error: {
+ message: 'message stub first'
+ }
+ }),
+ retries: [
+ mkTestResult({
+ error: {
+ message: 'message stub second'
+ }
+ })
+ ]
+ })
+ ]
+ })
+ ];
+
+ const result = groupErrors({suites});
+
+ assert.deepEqual(result, [
+ {
+ count: 1,
+ name: 'message stub first',
+ pattern: 'message stub first',
+ tests: {
+ 'default-suite default-state': ['default-bro']
+ }
+ },
+ {
+ count: 1,
+ name: 'message stub second',
+ pattern: 'message stub second',
+ tests: {
+ 'default-suite default-state': ['default-bro']
+ }
+ }
+ ]);
+ });
+
+ it('should collect errors from children recursively', () => {
+ const suites = [
+ mkSuite({
+ suitePath: ['suite'],
+ children: [
+ mkSuite({
+ suitePath: ['suite', 'state-one'],
+ children: [
+ mkState({
+ suitePath: ['suite', 'state-one', 'state-two'],
+ browsers: [
+ mkBrowserResult({
+ result: mkTestResult({
+ error: {
+ message: 'message stub'
+ }
+ })
+ })
+ ]
+ })
+ ]
+ })
+ ]
+ })
+ ];
+
+ const result = groupErrors({suites});
+
+ assert.deepEqual(result, [
+ {
+ count: 1,
+ name: 'message stub',
+ pattern: 'message stub',
+ tests: {
+ 'suite state-one state-two': ['default-bro']
+ }
+ }
+ ]);
+ });
+
+ it('should group errors from different browser but single test', () => {
+ const suites = [
+ mkSuiteTree({
+ browsers: [
+ mkBrowserResult({
+ name: 'browserOne',
+ result: mkTestResult({
+ error: {
+ message: 'message stub'
+ }
+ })
+ }),
+ mkBrowserResult({
+ name: 'browserTwo',
+ result: mkTestResult({
+ error: {
+ message: 'message stub'
+ }
+ })
+ })
+ ]
+ })
+ ];
+
+ const result = groupErrors({suites});
+
+ assert.deepEqual(result, [
+ {
+ count: 2,
+ name: 'message stub',
+ pattern: 'message stub',
+ tests: {
+ 'default-suite default-state': ['browserOne', 'browserTwo']
+ }
+ }
+ ]);
+ });
+
+ it('should filter by test name', () => {
+ const suites = [
+ mkSuite({
+ suitePath: ['suite'],
+ children: [
+ mkSuite({
+ suitePath: ['suite', 'state-one'],
+ browsers: [
+ mkBrowserResult({
+ result: mkTestResult({
+ error: {
+ message: 'message stub'
+ }
+ })
+ })
+ ]
+ }),
+ mkSuite({
+ suitePath: ['suite', 'state-two'],
+ browsers: [
+ mkBrowserResult({
+ result: mkTestResult({
+ error: {
+ message: 'message stub'
+ }
+ })
+ })
+ ]
+ })
+ ]
+ })
+ ];
+
+ const result = groupErrors({
+ suites,
+ testNameFilter: 'suite state-one'
+ });
+ assert.deepEqual(result, [
+ {
+ count: 1,
+ name: 'message stub',
+ pattern: 'message stub',
+ tests: {
+ 'suite state-one': ['default-bro']
+ }
+ }
+ ]);
+ });
+
+ it('should filter by browser', () => {
+ const suites = [
+ mkSuite({
+ suitePath: ['suite'],
+ children: [
+ mkSuite({
+ suitePath: ['suite', 'state'],
+ browsers: [
+ mkBrowserResult({
+ name: 'browser-one',
+ result: mkTestResult({
+ error: {
+ message: 'message stub'
+ }
+ })
+ }),
+ mkBrowserResult({
+ name: 'browser-two',
+ result: mkTestResult({
+ error: {
+ message: 'message stub'
+ }
+ })
+ })
+ ]
+ })
+ ]
+ })
+ ];
+
+ const result = groupErrors({
+ suites,
+ filteredBrowsers: ['browser-one']
+ });
+ assert.deepEqual(result, [
+ {
+ count: 1,
+ name: 'message stub',
+ pattern: 'message stub',
+ tests: {
+ 'suite state': ['browser-one']
+ }
+ }
+ ]);
+ });
+
+ it('should group by regexp', () => {
+ const suites = [
+ mkSuiteTree({
+ browsers: [
+ mkBrowserResult({
+ result: mkTestResult({
+ error: {
+ message: 'message stub first'
+ }
+ }),
+ retries: [
+ mkTestResult({
+ error: {
+ message: 'message stub second'
+ }
+ })
+ ]
+ })
+ ]
+ })
+ ];
+ const errorPatterns = [
+ {
+ name: 'Name group: message stub first',
+ pattern: 'message .* first'
+ }
+ ];
+
+ const result = groupErrors({suites, errorPatterns});
+ assert.deepEqual(result, [
+ {
+ count: 1,
+ name: 'message stub second',
+ pattern: 'message stub second',
+ tests: {
+ 'default-suite default-state': ['default-bro']
+ }
+ },
+ {
+ count: 1,
+ name: 'Name group: message stub first',
+ pattern: 'message .* first',
+ tests: {
+ 'default-suite default-state': ['default-bro']
+ }
+ }
+ ]);
+ });
+});
diff --git a/test/lib/static/modules/utils.js b/test/lib/static/modules/utils.js
index c9f1566fd..db9e5b087 100644
--- a/test/lib/static/modules/utils.js
+++ b/test/lib/static/modules/utils.js
@@ -1,9 +1,19 @@
'use strict';
const utils = require('../../../../lib/static/modules/utils');
-const {FAIL, ERROR, SUCCESS} = require('../../../../lib/constants/test-statuses');
-const {NO_REF_IMAGE_ERROR} = require('../../../../lib/constants/errors').getCommonErrors();
-const {mkSuite, mkState, mkBrowserResult} = require('../../../utils');
+const {
+ FAIL,
+ ERROR,
+ SUCCESS
+} = require('../../../../lib/constants/test-statuses');
+const {
+ NO_REF_IMAGE_ERROR
+} = require('../../../../lib/constants/errors').getCommonErrors();
+const {
+ mkSuite,
+ mkState,
+ mkBrowserResult
+} = require('../../../utils');
describe('static/modules/utils', () => {
describe('isAcceptable', () => {
@@ -34,62 +44,97 @@ describe('static/modules/utils', () => {
});
});
- describe('shouldSuiteBeShownByName', () => {
- const suite = mkSuite({
- suitePath: ['Some suite'],
- children: [
- mkState({
- suitePath: ['Some suite', 'test one']
- })
- ]
- });
+ describe('shouldSuiteBeShown', () => {
+ describe('testNameFilter', () => {
+ const suite = mkSuite({
+ suitePath: ['Some suite'],
+ children: [
+ mkState({
+ suitePath: ['Some suite', 'test one']
+ })
+ ]
+ });
- it('should be true if top-level title matches', () => {
- assert.isTrue(utils.shouldSuiteBeShownByName(suite, 'Some suite'));
- });
+ it('should be true if top-level title matches', () => {
+ assert.isTrue(utils.shouldSuiteBeShown({suite, testNameFilter: 'Some suite'}));
+ });
- it('should be true if bottom-level title matches', () => {
- assert.isTrue(utils.shouldSuiteBeShownByName(suite, 'test one'));
- });
+ it('should be true if bottom-level title matches', () => {
+ assert.isTrue(utils.shouldSuiteBeShown({suite, testNameFilter: 'test one'}));
+ });
- it('should be false if no matches found', () => {
- assert.isFalse(utils.shouldSuiteBeShownByName(suite, 'test two'));
- });
+ it('should be false if no matches found', () => {
+ assert.isFalse(utils.shouldSuiteBeShown({suite, testNameFilter: 'test two'}));
+ });
- it('should be true if full title matches', () => {
- assert.isTrue(utils.shouldSuiteBeShownByName(suite, 'Some suite test one'));
- });
+ it('should be true if full title matches', () => {
+ assert.isTrue(
+ utils.shouldSuiteBeShown({suite, testNameFilter: 'Some suite test one'})
+ );
+ });
- it('should be false if only part of only top-level title matches', () => {
- assert.isFalse(utils.shouldSuiteBeShownByName(suite, 'Some suite test two'));
- });
+ it('should be false if only part of only top-level title matches', () => {
+ assert.isFalse(
+ utils.shouldSuiteBeShown({suite, testNameFilter: 'Some suite test two'})
+ );
+ });
- it('should be false if only part of only bottom-level title matches', () => {
- assert.isFalse(utils.shouldSuiteBeShownByName(suite, 'Another suite test one'));
+ it('should be false if only part of only bottom-level title matches', () => {
+ assert.isFalse(
+ utils.shouldSuiteBeShown({suite, testNameFilter: 'Another suite test one'})
+ );
+ });
});
- });
- describe('shouldSuiteBeShownByBrowser', () => {
- const suite = mkSuite({
- children: [
- mkState({
- browsers: [
- mkBrowserResult({name: 'first-bro'})
- ]
- })
- ]
- });
+ describe('errorGroupTests', () => {
+ const suite = mkSuite({
+ suitePath: ['Some suite'],
+ children: [
+ mkState({
+ suitePath: ['Some suite', 'test one']
+ })
+ ]
+ });
- it('should be true if browser id is equal', () => {
- assert.isTrue(utils.shouldSuiteBeShownByBrowser(suite, ['first-bro']));
- });
+ it('should be false if top-level title matches', () => {
+ assert.isFalse(utils.shouldSuiteBeShown({suite, errorGroupTests: {'Some suite': []}}));
+ });
+
+ it('should be false if bottom-level title matches', () => {
+ assert.isFalse(utils.shouldSuiteBeShown({suite, errorGroupTests: {'test one': []}}));
+ });
+
+ it('should be false if no matches found', () => {
+ assert.isFalse(utils.shouldSuiteBeShown({suite, errorGroupTests: {'Some suite test two': []}}));
+ });
- it('should be false if browser id is not a strict match', () => {
- assert.isFalse(utils.shouldSuiteBeShownByBrowser(suite, ['first']));
+ it('should be true if full title matches', () => {
+ assert.isTrue(
+ utils.shouldSuiteBeShown({suite, errorGroupTests: {'Some suite test one': []}})
+ );
+ });
});
- it('should be false if browser id is not equal', () => {
- assert.isFalse(utils.shouldSuiteBeShownByBrowser(suite, ['second-bro']));
+ describe('filteredBrowsers', () => {
+ const suite = mkSuite({
+ children: [
+ mkState({
+ browsers: [mkBrowserResult({name: 'first-bro'})]
+ })
+ ]
+ });
+
+ it('should be true if browser id is equal', () => {
+ assert.isTrue(utils.shouldSuiteBeShown({suite, filteredBrowsers: ['first-bro']}));
+ });
+
+ it('should be false if browser id is not a strict match', () => {
+ assert.isFalse(utils.shouldSuiteBeShown({suite, filteredBrowsers: ['first']}));
+ });
+
+ it('should be false if browser id is not equal', () => {
+ assert.isFalse(utils.shouldSuiteBeShown({suite, filteredBrowsers: ['second-bro']}));
+ });
});
});