From b498f47ba69d75573763f373649922fbb1ba4052 Mon Sep 17 00:00:00 2001 From: Roman Kuznetsov Date: Fri, 20 Jan 2023 06:35:27 +0500 Subject: [PATCH] fix: add steps support --- lib/constants/database.js | 1 + lib/history-utils.js | 46 ++++++++++++----- lib/report-builder/static.js | 9 ++-- lib/sqlite-adapter.js | 2 + .../components/section/body/history/index.js | 29 +++++++++++ .../section/body/history/index.styl | 4 ++ lib/static/components/section/body/result.js | 2 + lib/test-adapter.js | 21 +++----- lib/tests-tree-builder/static.js | 1 + package-lock.json | 2 +- test/unit/lib/history-utils.js | 43 +++++++++++----- test/unit/lib/sqlite-adapter.js | 17 ++++--- .../static/components/section/body/history.js | 51 +++++++++++++++++++ .../static/components/section/body/result.js | 30 ++++++----- .../static/components/state/state-error.js | 5 +- test/unit/lib/test-adapter.js | 31 +++++++---- test/unit/lib/tests-tree-builder/static.js | 3 ++ 17 files changed, 219 insertions(+), 78 deletions(-) create mode 100644 lib/static/components/section/body/history/index.js create mode 100644 lib/static/components/section/body/history/index.styl create mode 100644 test/unit/lib/static/components/section/body/history.js 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,