diff --git a/lib/constants/database.js b/lib/constants/database.js
index 71edc2bbc..630cf5da4 100644
--- a/lib/constants/database.js
+++ b/lib/constants/database.js
@@ -7,6 +7,7 @@ const SUITES_TABLE_COLUMNS = [
{name: 'name', type: DB_TYPES.text},
{name: 'suiteUrl', type: DB_TYPES.text},
{name: 'metaInfo', type: DB_TYPES.text},
+ {name: 'history', type: DB_TYPES.text},
{name: 'description', type: DB_TYPES.text},
{name: 'error', type: DB_TYPES.text},
{name: 'skipReason', type: DB_TYPES.text},
diff --git a/lib/history-utils.js b/lib/history-utils.js
index f17a3c934..fa0177997 100644
--- a/lib/history-utils.js
+++ b/lib/history-utils.js
@@ -1,6 +1,6 @@
'use strict';
-const {last, isUndefined} = require('lodash');
+const {isEmpty} = require('lodash');
const formatDuration = (d) => `<- ${d}ms`;
@@ -8,25 +8,43 @@ const wrapArg = (arg) => `"${arg}"`;
const getCommand = ({
n: name,
- a: args = [],
- d: duration}
-) => `${name}(${args.map(wrapArg).join(', ')}) ${formatDuration(duration)}`;
+ a: args = []
+}) => `${name}(${args.map(wrapArg).join(', ')})`;
+
+const traverseNodes = (nodes, traverseCb, depth = 0) => {
+ nodes.forEach(node => {
+ const shouldTraverseChildren = traverseCb(node, depth);
+
+ if (shouldTraverseChildren) {
+ traverseNodes(node.c, traverseCb, depth + 1);
+ }
+ });
+};
const getCommandsHistory = (history) => {
- if (isUndefined(history)) {
+ if (isEmpty(history)) {
return;
}
try {
- const formatedCommands = history
- .map(getCommand)
- .map((s) => `\t${s}\n`);
- const lastCommandChildren = last(history).c;
- const formatedChildren = lastCommandChildren
- .map((cmd) => `${getCommand(cmd)}`)
- .map((s) => `\t\t${s}\n`);
-
- return [...formatedCommands, ...formatedChildren];
+ const formatedHistory = [];
+
+ const traverseCb = (node, depth) => {
+ const offset = '\t'.repeat(depth);
+ const isStep = node.n === 'runStep';
+ const duration = node.d;
+ const isFailed = !!node.f;
+ const title = isStep ? node.a[0] : getCommand(node);
+ const formatedDuration = formatDuration(duration);
+
+ formatedHistory.push(`${offset}${title} ${formatedDuration}\n`);
+
+ return isFailed;
+ };
+
+ traverseNodes(history, traverseCb);
+
+ return formatedHistory;
} catch (e) {
return `failed to get command history: ${e.message}`;
}
diff --git a/lib/report-builder/static.js b/lib/report-builder/static.js
index 1046f4dde..45f0c6ca4 100644
--- a/lib/report-builder/static.js
+++ b/lib/report-builder/static.js
@@ -38,7 +38,7 @@ module.exports = class StaticReportBuilder {
return result instanceof TestAdapter
? result
- : TestAdapter.create(result, this._hermione, this._pluginConfig, status);
+ : TestAdapter.create(result, this._hermione, status);
}
async saveStaticFiles() {
@@ -111,7 +111,8 @@ module.exports = class StaticReportBuilder {
_createTestResult(result, props) {
const {
- browserId, suite, sessionId, description, imagesInfo, screenshot, multipleTabs, errorDetails
+ browserId, suite, sessionId, description, history,
+ imagesInfo, screenshot, multipleTabs, errorDetails
} = result;
const {baseHost, saveErrorDetails} = this._pluginConfig;
@@ -119,8 +120,8 @@ module.exports = class StaticReportBuilder {
const metaInfo = _.merge(_.cloneDeep(result.meta), {url: suite.fullUrl, file: suite.file, sessionId});
const testResult = Object.assign({
- suiteUrl, name: browserId, metaInfo, description, imagesInfo,
- screenshot: Boolean(screenshot), multipleTabs
+ suiteUrl, name: browserId, metaInfo, description, history,
+ imagesInfo, screenshot: Boolean(screenshot), multipleTabs
}, props);
if (saveErrorDetails && errorDetails) {
diff --git a/lib/sqlite-adapter.js b/lib/sqlite-adapter.js
index df8d35c61..70b81ea78 100644
--- a/lib/sqlite-adapter.js
+++ b/lib/sqlite-adapter.js
@@ -66,6 +66,7 @@ module.exports = class SqliteAdapter {
name,
suiteUrl,
metaInfo,
+ history,
description,
error,
skipReason,
@@ -82,6 +83,7 @@ module.exports = class SqliteAdapter {
name,
suiteUrl,
metaInfo,
+ history,
description,
error,
skipReason,
diff --git a/lib/static/components/section/body/history/index.js b/lib/static/components/section/body/history/index.js
new file mode 100644
index 000000000..a00519993
--- /dev/null
+++ b/lib/static/components/section/body/history/index.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import {connect} from 'react-redux';
+import PropTypes from 'prop-types';
+import {isEmpty} from 'lodash';
+import Details from '../../../details';
+
+import './index.styl';
+
+const History = ({history}) => (
+ isEmpty(history)
+ ? null
+ :
+);
+
+History.propTypes = {
+ resultId: PropTypes.string.isRequired,
+ // from store
+ history: PropTypes.arrayOf(PropTypes.string).isRequired
+};
+
+export default connect(({tree}, {resultId}) => {
+ const {history = []} = tree.results.byId[resultId];
+
+ return {history};
+})(History);
diff --git a/lib/static/components/section/body/history/index.styl b/lib/static/components/section/body/history/index.styl
new file mode 100644
index 000000000..0d6c789d0
--- /dev/null
+++ b/lib/static/components/section/body/history/index.styl
@@ -0,0 +1,4 @@
+.history {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
diff --git a/lib/static/components/section/body/result.js b/lib/static/components/section/body/result.js
index b19aae863..c21415025 100644
--- a/lib/static/components/section/body/result.js
+++ b/lib/static/components/section/body/result.js
@@ -2,6 +2,7 @@ import React, {Component, Fragment} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import MetaInfo from './meta-info';
+import History from './history';
import Description from './description';
import Tabs from './tabs';
import ExtensionPoint from '../../extension-point';
@@ -26,6 +27,7 @@ class Result extends Component {
+
{result.description && }
diff --git a/lib/test-adapter.js b/lib/test-adapter.js
index 46d66adaa..c4832df8e 100644
--- a/lib/test-adapter.js
+++ b/lib/test-adapter.js
@@ -27,14 +27,13 @@ const globalCacheDiffImages = new Map();
const testsAttempts = new Map();
module.exports = class TestAdapter {
- static create(testResult = {}, hermione, pluginConfig, status) {
- return new this(testResult, hermione, pluginConfig, status);
+ static create(testResult = {}, hermione, status) {
+ return new this(testResult, hermione, status);
}
- constructor(testResult, hermione, pluginConfig, status) {
+ constructor(testResult, hermione, status) {
this._testResult = testResult;
this._hermione = hermione;
- this._pluginConfig = pluginConfig;
this._errors = this._hermione.errors;
this._suite = SuiteAdapter.create(this._testResult);
this._imagesSaver = this._hermione.htmlReporter.imagesSaver;
@@ -220,16 +219,12 @@ module.exports = class TestAdapter {
return this.imagesInfo;
}
- get error() {
- const err = _.pick(this._testResult.err, ['message', 'stack', 'stateName']);
- const {history} = this._testResult;
- const {commandsWithShortHistory} = this._pluginConfig;
-
- if (!_.isEmpty(history)) {
- err.history = getCommandsHistory(history, commandsWithShortHistory);
- }
+ get history() {
+ return getCommandsHistory(this._testResult.history);
+ }
- return err;
+ get error() {
+ return _.pick(this._testResult.err, ['message', 'stack', 'stateName']);
}
get imageDir() {
diff --git a/lib/tests-tree-builder/static.js b/lib/tests-tree-builder/static.js
index af4cad90d..55dddccc0 100644
--- a/lib/tests-tree-builder/static.js
+++ b/lib/tests-tree-builder/static.js
@@ -154,6 +154,7 @@ function mkTestResult(row, data = {}) {
description: row[DB_COLUMN_INDEXES.description],
imagesInfo: JSON.parse(row[DB_COLUMN_INDEXES.imagesInfo]),
metaInfo: JSON.parse(row[DB_COLUMN_INDEXES.metaInfo]),
+ history: JSON.parse(row[DB_COLUMN_INDEXES.history]),
multipleTabs: Boolean(row[DB_COLUMN_INDEXES.multipleTabs]),
name: row[DB_COLUMN_INDEXES.name],
screenshot: Boolean(row[DB_COLUMN_INDEXES.screenshot]),
diff --git a/package-lock.json b/package-lock.json
index f7928abf4..c004d7e64 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,7 +6,7 @@
"packages": {
"": {
"name": "html-reporter",
- "version": "9.7.2",
+ "version": "9.7.5",
"license": "MIT",
"dependencies": {
"@gemini-testing/sql.js": "^1.0.1",
diff --git a/test/unit/lib/history-utils.js b/test/unit/lib/history-utils.js
index e1456e1e6..352970383 100644
--- a/test/unit/lib/history-utils.js
+++ b/test/unit/lib/history-utils.js
@@ -12,7 +12,7 @@ describe('history-utils', () => {
afterEach(() => sandbox.restore());
- it('should return commands executed in test file and all sub commands of the last command', async () => {
+ it('should return commands executed in test file', () => {
const allHistory = [
{n: 'foo', a: ['foo-arg'], d: 10, c: []},
{n: 'baz', a: ['baz-arg'], d: 1, c: []},
@@ -23,28 +23,43 @@ describe('history-utils', () => {
]}
];
- const history = await getCommandsHistory(allHistory);
+ const history = getCommandsHistory(allHistory);
assert.deepEqual(history, [
- '\tfoo("foo-arg") <- 10ms\n',
- '\tbaz("baz-arg") <- 1ms\n',
- '\tbar("bar-arg") <- 3ms\n',
- '\tqux("qux-arg") <- 4ms\n',
- '\t\tqux("qux-arg") <- 4ms\n',
- '\t\tbaz("bar-arg") <- 3ms\n'
+ 'foo("foo-arg") <- 10ms\n',
+ 'baz("baz-arg") <- 1ms\n',
+ 'bar("bar-arg") <- 3ms\n',
+ 'qux("qux-arg") <- 4ms\n'
]);
});
- it('should return undefined if all history is not given', async () => {
- const history = await getCommandsHistory(undefined);
+ it('should return commands executed in test file and all sub commands of the failed command', () => {
+ const allHistory = [
+ {n: 'foo', a: ['foo-arg'], d: 10, c: []},
+ {n: 'baz', a: ['baz-arg'], d: 1, c: []},
+ {n: 'bar', a: ['bar-arg'], d: 3, c: []},
+ {n: 'qux', a: ['qux-arg'], d: 4, f: true, c: [
+ {n: 'qux', a: ['qux-arg'], d: 4, c: []},
+ {n: 'baz', a: ['bar-arg'], d: 3, f: true, c: []}
+ ]}
+ ];
+
+ const history = getCommandsHistory(allHistory);
- assert.isUndefined(history);
+ assert.deepEqual(history, [
+ 'foo("foo-arg") <- 10ms\n',
+ 'baz("baz-arg") <- 1ms\n',
+ 'bar("bar-arg") <- 3ms\n',
+ 'qux("qux-arg") <- 4ms\n',
+ '\tqux("qux-arg") <- 4ms\n',
+ '\tbaz("bar-arg") <- 3ms\n'
+ ]);
});
- it('should return failure message in case of exception', async () => {
- const history = await getCommandsHistory([{}]);
+ it('should return undefined if all history is not given', () => {
+ const history = getCommandsHistory(undefined);
- assert.match(history, /failed to get command history: .*/);
+ assert.isUndefined(history);
});
});
});
diff --git a/test/unit/lib/sqlite-adapter.js b/test/unit/lib/sqlite-adapter.js
index fbab1c020..30296ba18 100644
--- a/test/unit/lib/sqlite-adapter.js
+++ b/test/unit/lib/sqlite-adapter.js
@@ -40,14 +40,15 @@ describe('lib/sqlite-adapter', () => {
{cid: 2, name: 'name', type: 'TEXT'},
{cid: 3, name: 'suiteUrl', type: 'TEXT'},
{cid: 4, name: 'metaInfo', type: 'TEXT'},
- {cid: 5, name: 'description', type: 'TEXT'},
- {cid: 6, name: 'error', type: 'TEXT'},
- {cid: 7, name: 'skipReason', type: 'TEXT'},
- {cid: 8, name: 'imagesInfo', type: 'TEXT'},
- {cid: 9, name: 'screenshot', type: 'INT'},
- {cid: 10, name: 'multipleTabs', type: 'INT'},
- {cid: 11, name: 'status', type: 'TEXT'},
- {cid: 12, name: 'timestamp', type: 'INT'}
+ {cid: 5, name: 'history', type: 'TEXT'},
+ {cid: 6, name: 'description', type: 'TEXT'},
+ {cid: 7, name: 'error', type: 'TEXT'},
+ {cid: 8, name: 'skipReason', type: 'TEXT'},
+ {cid: 9, name: 'imagesInfo', type: 'TEXT'},
+ {cid: 10, name: 'screenshot', type: 'INT'},
+ {cid: 11, name: 'multipleTabs', type: 'INT'},
+ {cid: 12, name: 'status', type: 'TEXT'},
+ {cid: 13, name: 'timestamp', type: 'INT'}
];
const columns = db.prepare('PRAGMA table_info(suites);').all();
diff --git a/test/unit/lib/static/components/section/body/history.js b/test/unit/lib/static/components/section/body/history.js
new file mode 100644
index 000000000..ed13dd7cc
--- /dev/null
+++ b/test/unit/lib/static/components/section/body/history.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import {defaultsDeep, set} from 'lodash';
+import proxyquire from 'proxyquire';
+import {mkConnectedComponent} from '../../utils';
+
+describe('', () => {
+ const sandbox = sinon.sandbox.create();
+ let History, Details;
+
+ beforeEach(() => {
+ Details = sinon.stub().returns(null);
+
+ History = proxyquire('lib/static/components/section/body/history', {
+ '../../../details': {default: Details}
+ }).default;
+ });
+
+ afterEach(() => sandbox.restore());
+
+ const mkHistoryComponent = (props = {}, initialState = {}) => {
+ props = defaultsDeep(props, {
+ resultId: 'default-result'
+ });
+
+ return mkConnectedComponent(, {initialState});
+ };
+
+ it('should not render if history does not exists', () => {
+ const initialState = set({}, 'tree.results.byId.default-result', {});
+
+ mkHistoryComponent({resultId: 'default-result'}, initialState);
+
+ assert.notCalled(Details);
+ });
+
+ it('should render history if exists', () => {
+ const initialState = set({}, 'tree.results.byId.default-result.history', 'some-history');
+
+ const component = mkHistoryComponent({resultId: 'default-result'}, initialState);
+
+ assert.equal(component.find(Details).prop('content'), 'some-history');
+ });
+
+ it('should render with "History" title', () => {
+ const initialState = set({}, 'tree.results.byId.default-result.history', 'some-history');
+
+ const component = mkHistoryComponent({resultId: 'default-result'}, initialState);
+
+ assert.equal(component.find(Details).prop('title'), 'History');
+ });
+});
diff --git a/test/unit/lib/static/components/section/body/result.js b/test/unit/lib/static/components/section/body/result.js
index 15e3bc886..8269fe92d 100644
--- a/test/unit/lib/static/components/section/body/result.js
+++ b/test/unit/lib/static/components/section/body/result.js
@@ -1,12 +1,12 @@
import React from 'react';
import proxyquire from 'proxyquire';
-import {defaults} from 'lodash';
+import {defaults, set} from 'lodash';
import {FAIL, SUCCESS} from 'lib/constants/test-statuses';
import {mkConnectedComponent} from '../../utils';
describe('', () => {
const sandbox = sinon.sandbox.create();
- let Result, MetaInfo, Description, Tabs;
+ let Result, MetaInfo, History, Description, Tabs;
const mkResult = (props = {}, initialState = {}) => {
props = defaults(props, {
@@ -20,7 +20,8 @@ describe('', () => {
byId: {
'default-id': {
status: SUCCESS,
- imageIds: []
+ imageIds: [],
+ history: []
}
}
}
@@ -33,10 +34,12 @@ describe('', () => {
beforeEach(() => {
MetaInfo = sinon.stub().returns(null);
Description = sinon.stub().returns(null);
+ History = sinon.stub().returns(null);
Tabs = sinon.stub().returns(null);
Result = proxyquire('lib/static/components/section/body/result', {
'./meta-info': {default: MetaInfo},
+ './history': {default: History},
'./description': {default: Description},
'./tabs': {default: Tabs}
}).default;
@@ -47,15 +50,7 @@ describe('', () => {
describe('"MetaInfo" component', () => {
it('should render with result and test name props', () => {
const result = {status: FAIL, imageIds: ['image-1']};
- const initialState = {
- tree: {
- results: {
- byId: {
- 'result-1': result
- }
- }
- }
- };
+ const initialState = set({}, 'tree.results.byId.result-1', result);
mkResult({resultId: 'result-1', testName: 'test-name'}, initialState);
@@ -63,6 +58,17 @@ describe('', () => {
});
});
+ describe('"History" component', () => {
+ it('should render with resultId prop', () => {
+ const result = {status: FAIL, imageIds: ['image-1'], history: []};
+ const initialState = set({}, 'tree.results.byId.result-1', result);
+
+ mkResult({resultId: 'result-1', testName: 'test-name'}, initialState);
+
+ assert.calledOnceWith(History, {resultId: 'result-1'});
+ });
+ });
+
describe('"Description" component', () => {
it('should not render if description does not exists in result', () => {
const result = {status: FAIL, imageIds: [], description: null};
diff --git a/test/unit/lib/static/components/state/state-error.js b/test/unit/lib/static/components/state/state-error.js
index 1bc144dd4..d2eb0aadd 100644
--- a/test/unit/lib/static/components/state/state-error.js
+++ b/test/unit/lib/static/components/state/state-error.js
@@ -38,14 +38,13 @@ describe(' component', () => {
afterEach(() => sandbox.restore());
describe('"errorPatterns" is not specified', () => {
- it('should render error "message", "stack" and "history" if "errorPatterns" is empty', () => {
- const error = {message: 'some-msg', stack: 'some-stack', history: 'some-history'};
+ it('should render error "message" and "stack" if "errorPatterns" is empty', () => {
+ const error = {message: 'some-msg', stack: 'some-stack'};
const component = mkStateErrorComponent({result: {error}}, {config: {errorPatterns: []}});
assert.equal(component.find('.error__item').at(0).text(), 'message: some-msg');
assert.equal(component.find('.error__item').at(1).text(), 'stack: some-stack');
- assert.equal(component.find('.error__item').at(2).text(), 'history: some-history');
});
it('should break error fields by line break', () => {
diff --git a/test/unit/lib/test-adapter.js b/test/unit/lib/test-adapter.js
index e2439bdcb..cc49815a0 100644
--- a/test/unit/lib/test-adapter.js
+++ b/test/unit/lib/test-adapter.js
@@ -17,7 +17,7 @@ describe('hermione test adapter', () => {
class NoRefImageError extends Error {}
const mkHermioneTestResultAdapter = (testResult, {
- toolOpts = {}, pluginConfig = {}, htmlReporter = {}, status
+ toolOpts = {}, htmlReporter = {}, status
} = {}) => {
const config = _.defaults(toolOpts.config, {
browsers: {
@@ -36,7 +36,7 @@ describe('hermione test adapter', () => {
}, htmlReporter)
);
- return new HermioneTestResultAdapter(testResult, tool, pluginConfig, status);
+ return new HermioneTestResultAdapter(testResult, tool, status);
};
const mkTestResult_ = (result) => _.defaults(result, {
@@ -92,7 +92,7 @@ describe('hermione test adapter', () => {
assert.equal(result.attempt, 0);
});
- it('should return test error with "message", "stack", "history" and "stateName"', () => {
+ it('should return test error with "message", "stack" and "stateName"', () => {
getCommandsHistory.withArgs([{name: 'foo'}], ['foo']).returns(['some-history']);
const testResult = mkTestResult_({
file: 'bar',
@@ -105,20 +105,33 @@ describe('hermione test adapter', () => {
}
});
- const hermioneTestAdapter = mkHermioneTestResultAdapter(testResult, {
- pluginConfig: {
- commandsWithShortHistory: ['foo']
- }
- });
+ const hermioneTestAdapter = mkHermioneTestResultAdapter(testResult);
assert.deepEqual(hermioneTestAdapter.error, {
message: 'some-message',
stack: 'some-stack',
- history: ['some-history'],
stateName: 'some-test'
});
});
+ it('should return test history', () => {
+ getCommandsHistory.withArgs([{name: 'foo'}]).returns(['some-history']);
+ const testResult = mkTestResult_({
+ file: 'bar',
+ history: [{name: 'foo'}],
+ err: {
+ message: 'some-message',
+ stack: 'some-stack',
+ stateName: 'some-test',
+ foo: 'bar'
+ }
+ });
+
+ const hermioneTestAdapter = mkHermioneTestResultAdapter(testResult);
+
+ assert.deepEqual(hermioneTestAdapter.history, ['some-history']);
+ });
+
it('should return test state', () => {
const testResult = mkTestResult_({title: 'some-test'});
diff --git a/test/unit/lib/tests-tree-builder/static.js b/test/unit/lib/tests-tree-builder/static.js
index 2f8cb90fc..ccc6b84b7 100644
--- a/test/unit/lib/tests-tree-builder/static.js
+++ b/test/unit/lib/tests-tree-builder/static.js
@@ -16,6 +16,7 @@ describe('StaticResultsTreeBuilder', () => {
name: 'default-browser',
suiteUrl: 'default-url',
metaInfo: {},
+ history: [],
description: 'default-descr',
error: null,
skipReason: '',
@@ -34,6 +35,7 @@ describe('StaticResultsTreeBuilder', () => {
result.name,
result.suiteUrl,
JSON.stringify(result.metaInfo),
+ JSON.stringify(result.history),
result.description,
JSON.stringify(result.error),
result.skipReason,
@@ -50,6 +52,7 @@ describe('StaticResultsTreeBuilder', () => {
description: result.description,
imagesInfo: result.imagesInfo,
metaInfo: result.metaInfo,
+ history: result.history,
multipleTabs: result.multipleTabs,
name: result.name,
screenshot: result.screenshot,