diff --git a/README.md b/README.md index b450997a8..29f183917 100644 --- a/README.md +++ b/README.md @@ -300,12 +300,12 @@ tool.htmlReporter.addMetaInfoExtender(name, value); ``` * **name** (required) `String` - name of meta info -* **value** (required) `Function` - handler to which `suite` and `extraItems` are passed +* **value** (required) `Function` - handler to which `data` (`Object` with `testName` field) and `extraItems` are passed Example: ```js -tool.htmlReporter.addMetaInfoExtender('foo', (suite, extraItems) => { - return suite.suitePath.join(' ') + extraItems.platform; +tool.htmlReporter.addMetaInfoExtender('foo', (data, extraItems) => { + return data.testName + extraItems.platform; }); ``` diff --git a/lib/gui/app.js b/lib/gui/app.js index bd7c873f4..5ee8f7c19 100644 --- a/lib/gui/app.js +++ b/lib/gui/app.js @@ -1,19 +1,8 @@ 'use strict'; -const path = require('path'); const _ = require('lodash'); -const looksSame = require('looks-same'); -const Promise = require('bluebird'); const ToolRunner = require('./tool-runner'); -const lookSameAsync = (img1, img2, opts) => { - return new Promise((resolve, reject) => { - looksSame(img1, img2, opts, (err, data) => { - err ? reject(err) : resolve(data); - }); - }); -}; - module.exports = class App { static create(paths, hermione, configs) { return new this(paths, hermione, configs); @@ -54,53 +43,20 @@ module.exports = class App { .finally(() => this._restoreRetries()); } - updateReferenceImage(failedTests = []) { - return this._toolRunner.updateReferenceImage(failedTests); + getTestsDataToUpdateRefs(imageIds = []) { + return this._toolRunner.getTestsDataToUpdateRefs(imageIds); } - _resolveImgPath(imgPath) { - return path.resolve(process.cwd(), this._pluginConfig.path, imgPath); + getImageDataToFindEqualDiffs(imageIds = []) { + return this._toolRunner.getImageDataToFindEqualDiffs(imageIds); } - _getCompareOpts() { - const {tolerance, antialiasingTolerance} = this._toolRunner.config; - - return {tolerance, antialiasingTolerance, stopOnFirstFail: true, shouldCluster: false}; + updateReferenceImage(failedTests = []) { + return this._toolRunner.updateReferenceImage(failedTests); } - async findEqualDiffs(imagesInfo) { - const [refImagesInfo, ...comparedImagesInfo] = imagesInfo; - const compareOpts = this._getCompareOpts(); - - return await Promise.filter(comparedImagesInfo, async (imageInfo) => { - try { - await Promise.mapSeries(imageInfo.diffClusters, async (diffCluster, i) => { - const refComparisonRes = await lookSameAsync( - {source: this._resolveImgPath(refImagesInfo.expectedImg.path), boundingBox: refImagesInfo.diffClusters[i]}, - {source: this._resolveImgPath(imageInfo.expectedImg.path), boundingBox: diffCluster}, - compareOpts - ); - - if (!refComparisonRes.equal) { - return Promise.reject(false); - } - - const actComparisonRes = await lookSameAsync( - {source: this._resolveImgPath(refImagesInfo.actualImg.path), boundingBox: refImagesInfo.diffClusters[i]}, - {source: this._resolveImgPath(imageInfo.actualImg.path), boundingBox: diffCluster}, - compareOpts - ); - - if (!actComparisonRes.equal) { - return Promise.reject(false); - } - }); - - return true; - } catch (err) { - return err === false ? err : Promise.reject(err); - } - }); + async findEqualDiffs(data) { + return this._toolRunner.findEqualDiffs(data); } addClient(connection) { diff --git a/lib/gui/server.js b/lib/gui/server.js index 1f47b9107..e3c63acfb 100644 --- a/lib/gui/server.js +++ b/lib/gui/server.js @@ -74,12 +74,30 @@ exports.start = async ({paths, hermione, guiApi, configs}) => { res.sendStatus(OK); }); + server.post('/get-update-reference-data', (req, res) => { + try { + const data = app.getTestsDataToUpdateRefs(req.body); + res.json(data); + } catch (error) { + res.status(INTERNAL_SERVER_ERROR).send({error: error.message}); + } + }); + server.post('/update-reference', (req, res) => { app.updateReferenceImage(req.body) .then((updatedTests) => res.json(updatedTests)) .catch(({message}) => res.status(INTERNAL_SERVER_ERROR).send({error: message})); }); + server.post('/get-find-equal-diffs-data', (req, res) => { + try { + const data = app.getImageDataToFindEqualDiffs(req.body); + res.json(data); + } catch (error) { + res.status(INTERNAL_SERVER_ERROR).send({error: error.message}); + } + }); + server.post('/find-equal-diffs', async (req, res) => { try { const result = await app.findEqualDiffs(req.body); diff --git a/lib/gui/tool-runner/index.js b/lib/gui/tool-runner/index.js index 6b1c47df3..aec904d21 100644 --- a/lib/gui/tool-runner/index.js +++ b/lib/gui/tool-runner/index.js @@ -5,6 +5,7 @@ const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const Promise = require('bluebird'); +const looksSame = require('looks-same'); const Runner = require('./runner'); const subscribeOnToolEvents = require('./report-subscriber'); @@ -15,14 +16,15 @@ const reporterHelper = require('../../reporter-helpers'); const {UPDATED} = require('../../constants/test-statuses'); const constantFileNames = require('../../constants/file-names'); const logger = utils.logger; -const { - formatTests, - formatId, getShortMD5, - mkFullTitle, - mergeDatabasesForReuse, - getDataFromDatabase, - findTestResult -} = require('./utils'); +const {formatId, getShortMD5, mkFullTitle, mergeDatabasesForReuse, getDataFromDatabase, filterByEqualDiffSizes} = require('./utils'); + +const lookSameAsync = (img1, img2, opts) => { + return new Promise((resolve, reject) => { + looksSame(img1, img2, opts, (err, data) => { + err ? reject(err) : resolve(data); + }); + }); +}; module.exports = class ToolRunner { static create(paths, hermione, configs) { @@ -51,8 +53,7 @@ module.exports = class ToolRunner { } get tree() { - const {suites} = this._reportBuilder.getResult(); - return {...this._tree, suites}; + return this._tree; } async initialize() { @@ -86,16 +87,28 @@ module.exports = class ToolRunner { this._eventSource.emit(event, data); } - updateReferenceImage(tests) { - const reportBuilder = this._reportBuilder; + getTestsDataToUpdateRefs(imageIds) { + return this._reportBuilder.getTestsDataToUpdateRefs(imageIds); + } + + getImageDataToFindEqualDiffs(imageIds) { + const [selectedImage, ...comparedImages] = this._reportBuilder.getImageDataToFindEqualDiffs(imageIds); + + const imagesWithEqualBrowserName = comparedImages.filter((image) => image.browserName === selectedImage.browserName); + const imagesWithEqualDiffSizes = filterByEqualDiffSizes(imagesWithEqualBrowserName, selectedImage.diffClusters); + + return _.isEmpty(imagesWithEqualDiffSizes) ? [] : [selectedImage].concat(imagesWithEqualDiffSizes); + } + updateReferenceImage(tests) { return Promise.map(tests, (test) => { const updateResult = this._prepareUpdateResult(test); - const formattedResult = reportBuilder.format(updateResult, UPDATED); + const formattedResult = this._reportBuilder.format(updateResult, UPDATED); + const failResultId = formattedResult.id; - if (formattedResult.attempt < updateResult.attempt) { - formattedResult.attempt = updateResult.attempt; - } + const updateAttempt = this._reportBuilder.getUpdatedAttempt(formattedResult); + formattedResult.attempt = updateAttempt; + updateResult.attempt = updateAttempt; return Promise.map(updateResult.imagesInfo, (imageInfo) => { const {stateName} = imageInfo; @@ -107,16 +120,54 @@ module.exports = class ToolRunner { this._emitUpdateReference(result, stateName); }); }) - .then(() => reportBuilder.addUpdated(updateResult)) - .then(() => findTestResult(reportBuilder.getSuites(), formattedResult.prepareTestResult())); + .then(() => { + this._reportBuilder.addUpdated(updateResult, failResultId); + return this._reportBuilder.getTestBranch(formattedResult.id); + }); }); } + async findEqualDiffs(images) { + const [selectedImage, ...comparedImages] = images; + const {tolerance, antialiasingTolerance} = this.config; + const compareOpts = {tolerance, antialiasingTolerance, stopOnFirstFail: true, shouldCluster: false}; + const equalImages = await Promise.filter(comparedImages, async (image) => { + try { + await Promise.mapSeries(image.diffClusters, async (diffCluster, i) => { + const refComparisonRes = await lookSameAsync( + {source: this._resolveImgPath(selectedImage.expectedImg.path), boundingBox: selectedImage.diffClusters[i]}, + {source: this._resolveImgPath(image.expectedImg.path), boundingBox: diffCluster}, + compareOpts + ); + + if (!refComparisonRes.equal) { + return Promise.reject(false); + } + + const actComparisonRes = await lookSameAsync( + {source: this._resolveImgPath(selectedImage.actualImg.path), boundingBox: selectedImage.diffClusters[i]}, + {source: this._resolveImgPath(image.actualImg.path), boundingBox: diffCluster}, + compareOpts + ); + + if (!actComparisonRes.equal) { + return Promise.reject(false); + } + }); + + return true; + } catch (err) { + return err === false ? err : Promise.reject(err); + } + }); + + return equalImages.map((image) => image.id); + } + run(tests = []) { const {grep, set: sets, browser: browsers} = this._globalOpts; - const formattedTests = _.flatMap([].concat(tests), (test) => formatTests(test)); - return Runner.create(this._collection, formattedTests) + return Runner.create(this._collection, tests) .run((collection) => this._hermione.run(collection, {grep, sets, browsers})); } @@ -181,7 +232,7 @@ module.exports = class ToolRunner { this._reportBuilder.reuseTestsTree(testsTree); } - this._tree = {...this._reportBuilder.getResult(), gui: true, autoRun}; + this._tree = {...this._reportBuilder.getResult(), autoRun}; } async _loadDataFromDatabase() { @@ -195,4 +246,8 @@ module.exports = class ToolRunner { return {}; } + + _resolveImgPath(imgPath) { + return path.resolve(process.cwd(), this._pluginConfig.path, imgPath); + } }; diff --git a/lib/gui/tool-runner/report-subscriber.js b/lib/gui/tool-runner/report-subscriber.js index badebcb99..0db92670e 100644 --- a/lib/gui/tool-runner/report-subscriber.js +++ b/lib/gui/tool-runner/report-subscriber.js @@ -3,7 +3,7 @@ const clientEvents = require('../constants/client-events'); const {RUNNING} = require('../../constants/test-statuses'); const {getSuitePath} = require('../../plugin-utils').getHermioneUtils(); -const {findTestResult, withErrorHandling} = require('./utils'); +const {withErrorHandling} = require('./utils'); const createWorkers = require('../../workers/create-workers'); let workers; @@ -33,20 +33,19 @@ module.exports = (hermione, reportBuilder, client, reportPath) => { } client.emit(clientEvents.BEGIN_SUITE, { - name: suite.title, - suitePath: getSuitePath(suite), + suiteId: getSuitePath(suite).join(' '), status: RUNNING }); }); hermione.on(hermione.events.TEST_BEGIN, (data) => { - const {browserId} = data; + const formattedResult = reportBuilder.format(data, RUNNING); + formattedResult.attempt = reportBuilder.getCurrAttempt(formattedResult); - client.emit(clientEvents.BEGIN_STATE, { - suitePath: getSuitePath(data), - browserId, - status: RUNNING - }); + reportBuilder.addRunning(formattedResult); + const testBranch = reportBuilder.getTestBranch(formattedResult.id); + + return client.emit(clientEvents.BEGIN_STATE, testBranch); }); hermione.on(hermione.events.TEST_PASS, async (data) => { @@ -56,12 +55,9 @@ module.exports = (hermione, reportBuilder, client, reportPath) => { await formattedResult.saveTestImages(reportPath, workers); reportBuilder.addSuccess(formattedResult); - const testResult = findTestResult( - reportBuilder.getSuites(), - formattedResult.prepareTestResult() - ); - return client.emit(clientEvents.TEST_RESULT, testResult); + const testBranch = reportBuilder.getTestBranch(formattedResult.id); + return client.emit(clientEvents.TEST_RESULT, testBranch); }); }); @@ -74,9 +70,9 @@ module.exports = (hermione, reportBuilder, client, reportPath) => { formattedResult.hasDiff() ? reportBuilder.addFail(formattedResult) : reportBuilder.addError(formattedResult); - const testResult = findTestResult(reportBuilder.getSuites(), formattedResult.prepareTestResult()); - return client.emit(clientEvents.TEST_RESULT, testResult); + const testBranch = reportBuilder.getTestBranch(formattedResult.id); + return client.emit(clientEvents.TEST_RESULT, testBranch); }); }); @@ -87,6 +83,9 @@ module.exports = (hermione, reportBuilder, client, reportPath) => { await failHandler(formattedResult); reportBuilder.addRetry(formattedResult); + + const testBranch = reportBuilder.getTestBranch(formattedResult.id); + return client.emit(clientEvents.TEST_RESULT, testBranch); }); }); @@ -97,9 +96,9 @@ module.exports = (hermione, reportBuilder, client, reportPath) => { await failHandler(formattedResult); reportBuilder.addSkipped(formattedResult); - const testResult = findTestResult(reportBuilder.getSuites(), formattedResult.prepareTestResult()); - return client.emit(clientEvents.TEST_RESULT, testResult); + const testBranch = reportBuilder.getTestBranch(formattedResult.id); + return client.emit(clientEvents.TEST_RESULT, testBranch); }); }); diff --git a/lib/gui/tool-runner/runner/specific-test-runner.js b/lib/gui/tool-runner/runner/specific-test-runner.js index 54d20a6c1..0658c8336 100644 --- a/lib/gui/tool-runner/runner/specific-test-runner.js +++ b/lib/gui/tool-runner/runner/specific-test-runner.js @@ -1,7 +1,6 @@ 'use strict'; const Runner = require('./runner'); -const {mkFullTitle} = require('../utils'); module.exports = class SpecificTestRunner extends Runner { constructor(collection, tests) { @@ -19,8 +18,8 @@ module.exports = class SpecificTestRunner extends Runner { _filter() { this._collection.disableAll(); - this._tests.forEach((test) => { - this._collection.enableTest(mkFullTitle(test), test.browserId); + this._tests.forEach(({testName, browserName}) => { + this._collection.enableTest(testName, browserName); }); } }; diff --git a/lib/gui/tool-runner/utils.js b/lib/gui/tool-runner/utils.js index 6f81e3b18..c646a05ca 100644 --- a/lib/gui/tool-runner/utils.js +++ b/lib/gui/tool-runner/utils.js @@ -11,20 +11,9 @@ const NestedError = require('nested-error-stacks'); const StaticTestsTreeBuilder = require('../../tests-tree-builder/static'); const {logger, logError} = require('../../server-utils'); const constantFileNames = require('../../constants/file-names'); -const {findNode} = require('../../static/modules/utils'); const {mergeTablesQueries, selectAllSuitesQuery, compareDatabaseRowsByTimestamp} = require('../../common-utils'); const {writeDatabaseUrlsFile} = require('../../server-utils'); -const formatTestHandler = (browser, test) => { - const {suitePath, name} = test; - - return { - suite: {path: suitePath.slice(0, -1)}, - state: {name}, - browserId: browser.name - }; -}; - exports.formatId = (hash, browserId) => `${hash}/${browserId}`; exports.getShortMD5 = (str) => { @@ -36,32 +25,6 @@ exports.mkFullTitle = ({suite, state}) => { return `${suite.path.join(' ')} ${state.name}`; }; -exports.formatTests = (test) => { - let resultFromBrowsers = []; - let resultFromChildren = []; - - if (test.children) { - resultFromChildren = _.flatMap(test.children, (child) => exports.formatTests(child)); - } - - if (test.browsers) { - if (test.browserId) { - test.browsers = _.filter(test.browsers, {name: test.browserId}); - } - - resultFromBrowsers = _.flatMap(test.browsers, (browser) => formatTestHandler(browser, test)); - } - return [...resultFromBrowsers, ...resultFromChildren]; -}; - -exports.findTestResult = (suites = [], test) => { - const {name, suitePath, browserId} = test; - const nodeResult = findNode(suites, suitePath); - const browserResult = _.find(nodeResult.browsers, {name: browserId}); - - return {name, suitePath, browserId, browserResult}; -}; - // 'databaseUrls.json' may contain many databases urls but html-reporter at gui mode can work with single databases file. // all databases should be local files. exports.mergeDatabasesForReuse = async (reportPath) => { @@ -114,7 +77,7 @@ exports.getDataFromDatabase = (dbPath) => { const testsTreeBuilder = StaticTestsTreeBuilder.create(); const suitesRows = db.prepare(selectAllSuitesQuery()).raw().all().sort(compareDatabaseRowsByTimestamp); - const {tree} = testsTreeBuilder.build(suitesRows, {convertToOldFormat: false}); + const {tree} = testsTreeBuilder.build(suitesRows); db.close(); @@ -132,3 +95,65 @@ exports.withErrorHandling = async (fn) => { process.exit(1); } }; + +exports.filterByEqualDiffSizes = (imagesInfo, refDiffClusters) => { + if (_.isEmpty(refDiffClusters)) { + return []; + } + + const refDiffSizes = refDiffClusters.map(getDiffClusterSizes); + + return _.filter(imagesInfo, (imageInfo) => { + const imageDiffSizes = imageInfo.diffClusters.map(getDiffClusterSizes); + const equal = compareDiffSizes(imageDiffSizes, refDiffSizes); + + if (!equal) { + return false; + } + + if (!_.isEqual(imageDiffSizes, refDiffSizes)) { + imageInfo.diffClusters = reorderClustersByEqualSize(imageInfo.diffClusters, imageDiffSizes, refDiffSizes); + } + + return true; + }); +}; + +function getDiffClusterSizes(diffCluster) { + return { + width: diffCluster.right - diffCluster.left + 1, + height: diffCluster.bottom - diffCluster.top + 1 + }; +} + +function compareDiffSizes(diffSizes1, diffSizes2) { + if (diffSizes1.length !== diffSizes2.length) { + return false; + } + + return diffSizes1.every((diffSize) => { + const foundIndex = _.findIndex(diffSizes2, diffSize); + + if (foundIndex < 0) { + return false; + } + + diffSizes2 = diffSizes2.filter((v, ind) => ind !== foundIndex); + + return true; + }); +} + +function reorderClustersByEqualSize(diffClusters1, diffSizes1, diffSizes2) { + return diffClusters1.reduce((acc, cluster, i) => { + if (diffSizes1[i] !== diffSizes2[i]) { + const foundIndex = _.findIndex(diffSizes2, diffSizes1[i]); + diffSizes2 = diffSizes2.filter((v, ind) => ind !== foundIndex); + acc[foundIndex] = cluster; + } else { + acc[i] = cluster; + } + + return acc; + }, []); +} diff --git a/lib/report-builder/gui.js b/lib/report-builder/gui.js index bdcd759fe..08aa5fd66 100644 --- a/lib/report-builder/gui.js +++ b/lib/report-builder/gui.js @@ -3,8 +3,9 @@ const _ = require('lodash'); const StaticReportBuilder = require('./static'); const GuiTestsTreeBuilder = require('../tests-tree-builder/gui'); -const {IDLE, SKIPPED, FAIL, SUCCESS, UPDATED} = require('../constants/test-statuses'); -const {hasFails, allSkipped, hasNoRefImageErrors} = require('../static/modules/utils'); +const {IDLE, RUNNING, SKIPPED, FAIL, SUCCESS, UPDATED} = require('../constants/test-statuses'); +const {hasResultFails, hasNoRefImageErrors} = require('../static/modules/utils'); +const {isSkippedStatus} = require('../common-utils'); const {getConfigForStaticFile} = require('../server-utils'); module.exports = class GuiReportBuilder extends StaticReportBuilder { @@ -24,6 +25,10 @@ module.exports = class GuiReportBuilder extends StaticReportBuilder { return this._addTestResult(this.format(result, IDLE), {status: IDLE}); } + addRunning(result) { + return this._addTestResult(this.format(result, RUNNING), {status: RUNNING}); + } + addSkipped(result) { const formattedResult = super.addSkipped(result); const { @@ -37,14 +42,14 @@ module.exports = class GuiReportBuilder extends StaticReportBuilder { this._skips.push({suite, browser, comment}); } - addUpdated(result) { + addUpdated(result, failResultId) { const formattedResult = this.format(result, UPDATED); formattedResult.imagesInfo = [] .concat(result.imagesInfo) .map((imageInfo) => ({...imageInfo, ...formattedResult.getImagesFor(UPDATED, imageInfo.stateName)})); - return this._addTestResult(formattedResult, {status: UPDATED}); + return this._addTestResult(formattedResult, {status: UPDATED}, {failResultId}); } setApiValues(values) { @@ -64,7 +69,7 @@ module.exports = class GuiReportBuilder extends StaticReportBuilder { this._testsTree.sortTree(); return { - suites: this._testsTree.convertToOldFormat().suites, + tree: this._testsTree.tree, skips: this._skips, config, date: new Date().toString(), @@ -72,23 +77,39 @@ module.exports = class GuiReportBuilder extends StaticReportBuilder { }; } - getSuites() { - return this._testsTree.convertToOldFormat().suites; + getTestBranch(id) { + return this._testsTree.getTestBranch(id); + } + + getTestsDataToUpdateRefs(imageIds) { + return this._testsTree.getTestsDataToUpdateRefs(imageIds); + } + + getImageDataToFindEqualDiffs(imageId) { + return this._testsTree.getImageDataToFindEqualDiffs(imageId); } getCurrAttempt(formattedResult) { const {status, attempt} = this._testsTree.getLastResult(formattedResult); - return [IDLE, SKIPPED].includes(status) ? attempt : attempt + 1; + return [IDLE, RUNNING, SKIPPED].includes(status) ? attempt : attempt + 1; + } + + getUpdatedAttempt(formattedResult) { + const {attempt} = this._testsTree.getLastResult(formattedResult); + const imagesInfo = this._testsTree.getImagesInfo(formattedResult.id); + const isUpdated = imagesInfo.some((image) => image.status === UPDATED); + + return isUpdated ? attempt : attempt + 1; } - _addTestResult(formattedResult, props) { + _addTestResult(formattedResult, props, opts = {}) { super._addTestResult(formattedResult, props); const testResult = this._createTestResult(formattedResult, {...props, attempt: formattedResult.attempt}); - this._extendTestWithImagePaths(testResult, formattedResult); + this._extendTestWithImagePaths(testResult, formattedResult, opts); - if (testResult.status !== IDLE) { + if (![IDLE, RUNNING].includes(testResult.status)) { this._updateTestResultStatus(testResult, formattedResult); } @@ -98,7 +119,7 @@ module.exports = class GuiReportBuilder extends StaticReportBuilder { } _updateTestResultStatus(testResult, formattedResult) { - if (!hasFails({result: testResult}) && !allSkipped({result: testResult})) { + if (!hasResultFails(testResult) && !isSkippedStatus(testResult.status)) { testResult.status = SUCCESS; return; } @@ -114,17 +135,17 @@ module.exports = class GuiReportBuilder extends StaticReportBuilder { } } - _extendTestWithImagePaths(test, formattedResult) { + _extendTestWithImagePaths(test, formattedResult, opts = {}) { const newImagesInfo = formattedResult.getImagesInfo(test.status); if (test.status !== UPDATED) { return _.set(test, 'imagesInfo', newImagesInfo); } - const prevImagesInfo = this._testsTree.getImagesInfo(formattedResult); + const failImagesInfo = this._testsTree.getImagesInfo(opts.failResultId); - if (prevImagesInfo.length) { - test.imagesInfo = prevImagesInfo; + if (failImagesInfo.length) { + test.imagesInfo = _.clone(failImagesInfo); newImagesInfo.forEach((imageInfo) => { const {stateName} = imageInfo; diff --git a/lib/server-utils.js b/lib/server-utils.js index 587d41d7e..e736f3591 100644 --- a/lib/server-utils.js +++ b/lib/server-utils.js @@ -7,7 +7,7 @@ const chalk = require('chalk'); const _ = require('lodash'); const fs = require('fs-extra'); -const {ERROR, UPDATED, IDLE, SKIPPED} = require('./constants/test-statuses'); +const {ERROR, UPDATED, RUNNING, IDLE, SKIPPED} = require('./constants/test-statuses'); const {IMAGES_PATH} = require('./constants/paths'); const DATA_FILE_NAME = 'data.js'; @@ -90,7 +90,7 @@ function prepareCommonJSData(data) { } function shouldUpdateAttempt(status) { - return ![SKIPPED, UPDATED, IDLE].includes(status); + return ![SKIPPED, UPDATED, RUNNING, IDLE].includes(status); } function getDetailsFileName(testId, browserId, attempt) { diff --git a/lib/static/components/controls/accept-opened-button.js b/lib/static/components/controls/accept-opened-button.js new file mode 100644 index 000000000..89d2fa287 --- /dev/null +++ b/lib/static/components/controls/accept-opened-button.js @@ -0,0 +1,39 @@ +import React, {Component} from 'react'; +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; +import PropTypes from 'prop-types'; +import * as actions from '../../modules/actions'; +import ControlButton from './control-button'; +import {getAcceptableOpenedImageIds} from '../../modules/selectors/tree'; + +class AcceptOpenedButton extends Component { + static propTypes = { + // from store + processing: PropTypes.bool.isRequired, + acceptableOpenedImageIds: PropTypes.arrayOf(PropTypes.string).isRequired + } + + _acceptOpened = () => { + this.props.actions.acceptOpened(this.props.acceptableOpenedImageIds); + } + + render() { + const {acceptableOpenedImageIds, processing} = this.props; + + return ; + } +} + +export default connect( + (state) => { + return { + processing: state.processing, + acceptableOpenedImageIds: getAcceptableOpenedImageIds(state) + }; + }, + (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) +)(AcceptOpenedButton); diff --git a/lib/static/components/controls/base-host-input.js b/lib/static/components/controls/base-host-input.js index d61aa12e8..a571bbb6c 100644 --- a/lib/static/components/controls/base-host-input.js +++ b/lib/static/components/controls/base-host-input.js @@ -34,6 +34,6 @@ class BaseHostInput extends Component { } export default connect( - ({reporter: state}) => ({baseHost: state.view.baseHost}), + (state) => ({baseHost: state.view.baseHost}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(BaseHostInput); diff --git a/lib/static/components/controls/common-controls.js b/lib/static/components/controls/common-controls.js index 6f946b279..ab10d5527 100644 --- a/lib/static/components/controls/common-controls.js +++ b/lib/static/components/controls/common-controls.js @@ -78,6 +78,6 @@ class ControlButtons extends Component { } export default connect( - ({reporter: {view}}) => ({view}), + ({view}) => ({view}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(ControlButtons); diff --git a/lib/static/components/controls/common-filters.js b/lib/static/components/controls/common-filters.js index c09138abb..0ac9c4c7a 100644 --- a/lib/static/components/controls/common-filters.js +++ b/lib/static/components/controls/common-filters.js @@ -10,13 +10,13 @@ import BrowserList from './browser-list'; class CommonFilters extends Component { render() { - const {view, browsers, actions} = this.props; + const {filteredBrowsers, browsers, actions} = this.props; return (
@@ -27,6 +27,6 @@ class CommonFilters extends Component { } export default connect( - ({reporter: {view, browsers}}) => ({view, browsers}), + ({view, browsers}) => ({filteredBrowsers: view.filteredBrowsers, browsers}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(CommonFilters); diff --git a/lib/static/components/controls/custom-gui-controls.js b/lib/static/components/controls/custom-gui-controls.js index ce862fd1e..ab5a9f03e 100644 --- a/lib/static/components/controls/custom-gui-controls.js +++ b/lib/static/components/controls/custom-gui-controls.js @@ -65,6 +65,6 @@ class CustomGuiControls extends PureComponent { } export default connect( - ({reporter: state}) => ({customGui: state.config.customGui}), + (state) => ({customGui: state.config.customGui}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(CustomGuiControls); diff --git a/lib/static/components/controls/find-same-diffs-button.js b/lib/static/components/controls/find-same-diffs-button.js new file mode 100644 index 000000000..ee3e269fc --- /dev/null +++ b/lib/static/components/controls/find-same-diffs-button.js @@ -0,0 +1,47 @@ +import React, {Component} from 'react'; +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; +import PropTypes from 'prop-types'; +import * as actions from '../../modules/actions'; +import ControlButton from './control-button'; +import {getFailedOpenedImageIds} from '../../modules/selectors/tree'; + +class FindSameDiffsButton extends Component { + static propTypes = { + imageId: PropTypes.string, + browserId: PropTypes.string.isRequired, + isDisabled: PropTypes.bool.isRequired, + // from store + browserName: PropTypes.string.isRequired, + failedOpenedImageIds: PropTypes.arrayOf(PropTypes.string).isRequired + } + + _findSameDiffs = () => { + const {actions, imageId, failedOpenedImageIds, browserName} = this.props; + + actions.findSameDiffs(imageId, failedOpenedImageIds, browserName); + } + + render() { + const {isDisabled} = this.props; + + return ; + } +} + +export default connect( + (state, {browserId}) => { + const {name: browserName} = state.tree.browsers.byId[browserId]; + + return { + browserName, + failedOpenedImageIds: getFailedOpenedImageIds(state) + }; + }, + (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) +)(FindSameDiffsButton); diff --git a/lib/static/components/controls/gui-controls.js b/lib/static/components/controls/gui-controls.js index 2e51d3590..53088049a 100644 --- a/lib/static/components/controls/gui-controls.js +++ b/lib/static/components/controls/gui-controls.js @@ -1,43 +1,41 @@ -'use strict'; - import React, {Component} from 'react'; import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {pick, values} from 'lodash'; import PropTypes from 'prop-types'; import * as actions from '../../modules/actions'; import CommonControls from './common-controls'; import CustomGuiControls from './custom-gui-controls'; import ControlButton from './control-button'; import RunButton from './run-button'; +import AcceptOpenedButton from './accept-opened-button'; import CommonFilters from './common-filters'; +import {getFailedTests} from '../../modules/selectors/tree'; import './controls.less'; import './gui-controls.css'; class GuiControls extends Component { static propTypes = { - suiteIds: PropTypes.object, - running: PropTypes.bool, - processing: PropTypes.bool, - autoRun: PropTypes.bool, - failed: PropTypes.array + // from store + running: PropTypes.bool.isRequired, + processing: PropTypes.bool.isRequired, + autoRun: PropTypes.bool.isRequired, + allRootSuiteIds: PropTypes.arrayOf(PropTypes.string).isRequired, + failedRootSuiteIds: PropTypes.arrayOf(PropTypes.string).isRequired, + failedTests: PropTypes.arrayOf(PropTypes.shape({ + testName: PropTypes.string, + browserName: PropTypes.string + })).isRequired } _runFailedTests = () => { - const {actions, failed} = this.props; - - return actions.runFailedTests(failed); - } + const {actions, failedTests} = this.props; - _acceptOpened = () => { - const {actions, failed} = this.props; - - return actions.acceptOpened(failed); + return actions.runFailedTests(failedTests); } render() { - const {actions, suiteIds, failed, running, autoRun, processing} = this.props; + const {actions, allRootSuiteIds, failedRootSuiteIds, running, autoRun, processing} = this.props; return (
@@ -45,20 +43,16 @@ class GuiControls extends Component {
- +
@@ -68,9 +62,15 @@ class GuiControls extends Component { } export default connect( - ({reporter: {suiteIds, running, processing, autoRun, suites}}) => ({ - suiteIds, running, processing, autoRun, - failed: values(pick(suites, suiteIds.failed)) - }), + (state) => { + return { + running: state.running, + processing: state.processing, + autoRun: state.autoRun, + allRootSuiteIds: state.tree.suites.allRootIds, + failedRootSuiteIds: state.tree.suites.failedRootIds, + failedTests: getFailedTests(state) + }; + }, (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(GuiControls); diff --git a/lib/static/components/controls/menu-bar.js b/lib/static/components/controls/menu-bar.js index b33d23de9..0c59d3a90 100644 --- a/lib/static/components/controls/menu-bar.js +++ b/lib/static/components/controls/menu-bar.js @@ -36,4 +36,4 @@ class MenuBar extends Component { } } -export default connect(({reporter: {apiValues: {extraItems}}}) => ({extraItems}))(MenuBar); +export default connect(({apiValues: {extraItems}}) => ({extraItems}))(MenuBar); diff --git a/lib/static/components/controls/strict-match-filter-input.js b/lib/static/components/controls/strict-match-filter-input.js index c43318f1f..b64a97b58 100644 --- a/lib/static/components/controls/strict-match-filter-input.js +++ b/lib/static/components/controls/strict-match-filter-input.js @@ -43,6 +43,6 @@ class StrictMatchFilterInput extends Component { } export default connect( - ({reporter: state}) => ({strictMatchFilter: state.view.strictMatchFilter}), + (state) => ({strictMatchFilter: state.view.strictMatchFilter}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(StrictMatchFilterInput); diff --git a/lib/static/components/controls/test-name-filter-input.js b/lib/static/components/controls/test-name-filter-input.js index 144ca49b6..edf212b1d 100644 --- a/lib/static/components/controls/test-name-filter-input.js +++ b/lib/static/components/controls/test-name-filter-input.js @@ -49,6 +49,6 @@ class TestNameFilterInput extends Component { } export default connect( - ({reporter: state}) => ({testNameFilter: state.view.testNameFilter}), + (state) => ({testNameFilter: state.view.testNameFilter}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(TestNameFilterInput); diff --git a/lib/static/components/controls/view-select.js b/lib/static/components/controls/view-select.js index 0e138711a..501ed7724 100644 --- a/lib/static/components/controls/view-select.js +++ b/lib/static/components/controls/view-select.js @@ -9,7 +9,7 @@ import * as actions from '../../modules/actions'; class ViewSelect extends Component { static propTypes = { - view: PropTypes.object.isRequired, + viewMode: PropTypes.string.isRequired, actions: PropTypes.object.isRequired, options: PropTypes.array.isRequired } @@ -23,7 +23,7 @@ class ViewSelect extends Component { } render() { - const {view, options} = this.props; + const {viewMode, options} = this.props; const formatedOpts = options.map(({value, text}) => ({ value, text, @@ -36,7 +36,7 @@ class ViewSelect extends Component { fluid selection options={formatedOpts} - value={view.viewMode} + value={viewMode} onChange={this._onChange} />
@@ -45,6 +45,6 @@ class ViewSelect extends Component { } export default connect( - ({reporter: state}) => ({view: state.view}), + ({view}) => ({viewMode: view.viewMode}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(ViewSelect); diff --git a/lib/static/components/custom-scripts.js b/lib/static/components/custom-scripts.js index 3d933fd28..fddf20e9b 100644 --- a/lib/static/components/custom-scripts.js +++ b/lib/static/components/custom-scripts.js @@ -1,5 +1,3 @@ -'use strict'; - import React, {useEffect, useRef} from 'react'; import {isEmpty} from 'lodash'; diff --git a/lib/static/components/error-groups/item.js b/lib/static/components/error-groups/item.js index fe8ba9a44..a8378a742 100644 --- a/lib/static/components/error-groups/item.js +++ b/lib/static/components/error-groups/item.js @@ -1,5 +1,3 @@ -'use strict'; - import React, {Component} from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -20,12 +18,12 @@ export default class ErrorGroupsItem extends Component { } render() { - const {name, pattern, count, tests} = this.props.group; + const {name, pattern, count, browserIds} = this.props.group; const body = this.state.collapsed ? null :
- +
; const className = classNames( diff --git a/lib/static/components/error-groups/list.js b/lib/static/components/error-groups/list.js index 8150b1e08..50d4235dd 100644 --- a/lib/static/components/error-groups/list.js +++ b/lib/static/components/error-groups/list.js @@ -1,5 +1,3 @@ -'use strict'; - import React, {Component} from 'react'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; @@ -26,4 +24,6 @@ class ErrorGroupsList extends Component { } } -export default connect(({reporter: {groupedErrors}}) => ({groupedErrors}))(ErrorGroupsList); +export default connect( + ({groupedErrors}) => ({groupedErrors}) +)(ErrorGroupsList); diff --git a/lib/static/components/gui.js b/lib/static/components/gui.js index b53c000e4..d878755d6 100644 --- a/lib/static/components/gui.js +++ b/lib/static/components/gui.js @@ -1,30 +1,71 @@ -'use strict'; - import React, {Component, Fragment} from 'react'; import {connect} from 'react-redux'; +import PropTypes from 'prop-types'; import Notifications from 'reapop'; import wybo from 'reapop-theme-wybo'; +import {bindActionCreators} from 'redux'; -import {initial} from '../modules/actions'; +import * as actions from '../modules/actions'; import ControlButtons from './controls/gui-controls'; import SkippedList from './skipped-list'; import Loading from './loading'; import ModalContainer from '../containers/modal'; import MainTree from './main-tree'; import CustomScripts from './custom-scripts'; +import clientEvents from '../../gui/constants/client-events'; class Gui extends Component { + static propTypes = { + // from store + loading: PropTypes.shape({ + active: PropTypes.bool, + content: PropTypes.string + }).isRequired, + customScripts: PropTypes.array + } + componentDidMount() { - this.props.gui && this.props.initial(); + this.props.actions.initGuiReport(); + 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, () => { + actions.testsEnd(); + }); } render() { - const {loading, customScripts} = this.props; + const {allRootSuiteIds, loading, customScripts} = this.props; + + if (!allRootSuiteIds.length) { + return (); + } return ( - - + +
@@ -37,6 +78,11 @@ class Gui extends Component { } } -export default connect(({reporter: {gui, loading, config: {customScripts}}}) => { - return {gui, loading, customScripts}; -}, {initial})(Gui); +export default connect( + ({tree, loading, config: {customScripts}}) => ({ + allRootSuiteIds: tree.suites.allRootIds, + loading, + customScripts + }), + (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) +)(Gui); diff --git a/lib/static/components/header/index.js b/lib/static/components/header/index.js index d4aae5aa6..52108873b 100644 --- a/lib/static/components/header/index.js +++ b/lib/static/components/header/index.js @@ -28,6 +28,6 @@ class Header extends Component { } } -export default connect(({reporter: state}) => { +export default connect((state) => { return {date: state.date}; })(Header); diff --git a/lib/static/components/header/summary/dbSummary.js b/lib/static/components/header/summary/dbSummary.js index 7f1a5a0e9..c92dcbe24 100644 --- a/lib/static/components/header/summary/dbSummary.js +++ b/lib/static/components/header/summary/dbSummary.js @@ -1,5 +1,3 @@ -'use strict'; - import React, {Component} from 'react'; import {Dropdown, Popup, Button} from 'semantic-ui-react'; import PropTypes from 'prop-types'; @@ -11,7 +9,11 @@ export default class DbSummaryKey extends Component { Used when building a report using data from sqlite databases. */ static propTypes = { - fetchDbDetails: PropTypes.array + fetchDbDetails: PropTypes.arrayOf(PropTypes.shape({ + url: PropTypes.string, + status: PropTypes.number, + success: PropTypes.bool + })).isRequired }; state = { collapsed: true diff --git a/lib/static/components/header/summary/index.js b/lib/static/components/header/summary/index.js index 9938d93a4..807ccebe2 100644 --- a/lib/static/components/header/summary/index.js +++ b/lib/static/components/header/summary/index.js @@ -1,11 +1,9 @@ -'use strict'; - import React, {Component} from 'react'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; import SummaryKey from './item'; import DbSummaryKey from './dbSummary'; -import {getStats} from '../../../modules/utils'; +import {getStatsFilteredByBrowsers} from '../../../modules/selectors/stats'; import './summary.css'; @@ -38,13 +36,8 @@ class Summary extends Component { } export default connect( - ({reporter: state}) => { - const {stats} = state; - const {filteredBrowsers} = state.view; - const statsToShow = getStats(stats, filteredBrowsers); - - return { - stats: statsToShow, - fetchDbDetails: state.fetchDbDetails - }; - })(Summary); + (state) => ({ + stats: getStatsFilteredByBrowsers(state), + fetchDbDetails: state.fetchDbDetails + }) +)(Summary); diff --git a/lib/static/components/main-tree.js b/lib/static/components/main-tree.js index 3fe486150..3dfd64495 100644 --- a/lib/static/components/main-tree.js +++ b/lib/static/components/main-tree.js @@ -1,85 +1,23 @@ -'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 {openDbConnection, closeDbConnection, suiteBegin, testBegin, testResult, testsEnd} from '../modules/actions'; -import Loading from './loading'; class MainTree extends Component { static propTypes = { - gui: PropTypes.bool, + // from store groupByError: PropTypes.bool.isRequired } - componentDidMount() { - this.props.gui && this._subscribeToEvents(); - - if (!this.props.gui) { - this.props.actions.openDbConnection(); - } - } - - componentWillUnmount() { - if (!this.props.gui) { - this.props.actions.closeDbConnection(); - } - } - - _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, suites, fetchDbDetails} = this.props; - - if (!Object.keys(suites).length && !fetchDbDetails) { - return (); - } - - return groupByError - ? - : ; + return this.props.groupByError + ? + : ; } } -const actions = {testBegin, suiteBegin, testResult, testsEnd, openDbConnection, closeDbConnection}; - export default connect( - ({reporter: state}) => { - const {groupByError} = state.view; - return ({ - gui: state.gui, - groupByError, - fetchDbDetails: state.fetchDbDetails, - suites: state.suites - }); - }, - (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) + ({view: {groupByError}}) => ({groupByError}) )(MainTree); diff --git a/lib/static/components/report.js b/lib/static/components/report.js index 07ebadf0b..44cc0202f 100644 --- a/lib/static/components/report.js +++ b/lib/static/components/report.js @@ -1,7 +1,12 @@ -'use strict'; - import React, {Component, Fragment} from 'react'; import {connect} from 'react-redux'; +import PropTypes from 'prop-types'; +import Notifications from 'reapop'; +import wybo from 'reapop-theme-wybo'; +import {bindActionCreators} from 'redux'; + +import * as actions from '../modules/actions'; +import Loading from './loading'; import Header from './header'; import ControlButtons from './controls/report-controls'; import SkippedList from './skipped-list'; @@ -9,10 +14,32 @@ import MainTree from './main-tree'; import CustomScripts from './custom-scripts'; class Report extends Component { + static propTypes = { + // from store + allRootSuiteIds: PropTypes.arrayOf(PropTypes.string).isRequired, + fetchDbDetails: PropTypes.array.isRequired, + customScripts: PropTypes.array + } + + componentDidMount() { + this.props.actions.initStaticReport(); + } + + componentWillUnmount() { + this.props.actions.finStaticReport(); + } + render() { + const {allRootSuiteIds, fetchDbDetails} = this.props; + + if (!allRootSuiteIds.length && !fetchDbDetails.length) { + return (); + } + return ( +
@@ -24,4 +51,11 @@ class Report extends Component { } } -export default connect(({reporter: {config}}) => ({customScripts: config.customScripts}))(Report); +export default connect( + ({tree, fetchDbDetails, config}) => ({ + allRootSuiteIds: tree.suites.allRootIds, + fetchDbDetails, + customScripts: config.customScripts + }), + (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) +)(Report); diff --git a/lib/static/components/section/body/index.js b/lib/static/components/section/body/index.js index 0894d95e0..f8b143496 100644 --- a/lib/static/components/section/body/index.js +++ b/lib/static/components/section/body/index.js @@ -1,114 +1,74 @@ -'use strict'; - -import {isEmpty, defaults, pick, values, mapValues, omitBy} from 'lodash'; import React, {Component} from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import PropTypes from 'prop-types'; -import SwitcherRetry from '../switcher-retry'; +import RetrySwitcher from './retry-switcher'; import ControlButton from '../../controls/control-button'; -import State from '../../state'; -import MetaInfo from './meta-info'; -import Description from './description'; +import Result from './result'; import * as actions from '../../../modules/actions'; -import {isSuccessStatus, isErroredStatus} from '../../../../common-utils'; class Body extends Component { static propTypes = { - result: PropTypes.object.isRequired, - retries: PropTypes.array, - suite: PropTypes.object, - browser: PropTypes.object - } - - static defaultProps = { - retries: [] + browserId: PropTypes.string.isRequired, + browserName: PropTypes.string.isRequired, + testName: PropTypes.string.isRequired, + resultIds: PropTypes.array.isRequired, + // from store + gui: PropTypes.bool.isRequired, + running: PropTypes.bool.isRequired, + retryIndex: PropTypes.number } constructor(props) { super(props); const retry = typeof this.props.retryIndex === 'number' - ? Math.min(this.props.retryIndex, this.props.retries.length) - : this.props.retries.length; + ? Math.min(this.props.retryIndex, this.props.resultIds.length - 1) + : this.props.resultIds.length - 1; - this.state = { - retry - }; + this.state = {retry}; + this._changeTestRetry({retryIndex: retry}); } - componentDidMount() { - this._toggleTestResult({opened: true}); - this._changeTestRetry({retryIndex: this.state.retry}); - } - - componentWillUnmount() { - this._toggleTestResult({opened: false}); + componentWillReceiveProps(nextProps) { + if (nextProps.resultIds.length > this.props.resultIds.length) { + const lastRetryIndex = nextProps.resultIds.length - 1; + this.setState({retry: lastRetryIndex}); + this._changeTestRetry({retryIndex: lastRetryIndex}); + } } - onSwitcherRetryChange = (index) => { + onRetrySwitcherChange = (index) => { this.setState({retry: index}); this._changeTestRetry({retryIndex: index}); } - onTestAccept = (stateName) => { - const {result, suite} = this.props; - - this.props.actions.acceptTest(suite, result.name, stateName); - } - - onTestFindSameDiffs = (stateName) => { - const {suite: {suitePath}, browser, failed} = this.props; - - this.props.actions.findSameDiffs({suitePath, browser, stateName, fails: failed}); - } - onTestRetry = () => { - const {result, suite} = this.props; - - this.props.actions.retryTest(suite, result.name); - } - - onToggleStateResult = ({stateName, opened}) => { - const {result: {name: browserId}, suite: {suitePath}} = this.props; - const retryIndex = this.state.retry; - - this.props.actions.toggleStateResult({browserId, suitePath, stateName, retryIndex, opened}); - } - - getExtraMetaInfo = () => { - const {suite, apiValues: {extraItems, metaInfoExtenders}} = this.props; - - return omitBy(mapValues(metaInfoExtenders, (extender) => { - const stringifiedFn = extender.startsWith('return') ? extender : `return ${extender}`; - - return new Function(stringifiedFn)()(suite, extraItems); - }), isEmpty); - } + const {testName, browserName} = this.props; - _toggleTestResult({opened}) { - const {result: {name: browserId}, suite: {suitePath}} = this.props; - - this.props.actions.toggleTestResult({browserId, suitePath, opened}); + this.props.actions.retryTest({testName, browserName}); } _changeTestRetry({retryIndex}) { - const {result: {name: browserId}, suite: {suitePath}} = this.props; + const {browserId} = this.props; - this.props.actions.changeTestRetry({browserId, suitePath, retryIndex}); + this.props.actions.changeTestRetry({browserId, retryIndex}); } _addRetrySwitcher = () => { - const {retries, result} = this.props; - const testResults = retries.concat(result); + const {resultIds} = this.props; - if (testResults.length <= 1) { + if (resultIds.length <= 1) { return; } return (
- +
); } @@ -130,60 +90,13 @@ class Body extends Component { : null; } - _getActiveResult = () => { - const {result, retries} = this.props; - - return retries.concat(result)[this.state.retry]; - } - - _getTabs() { - const activeResult = this._getActiveResult(); - const {errorDetails} = activeResult; - const retryIndex = this.state.retry; - - if (isEmpty(activeResult.imagesInfo)) { - return isSuccessStatus(activeResult.status) ? null : this._drawTab(activeResult, {errorDetails}); - } - - const tabs = activeResult.imagesInfo.map((imageInfo, idx) => { - const {stateName} = imageInfo; - const error = imageInfo.error || activeResult.error; - - const options = imageInfo.stateName ? {} : {errorDetails}; - - return this._drawTab(imageInfo, {...options, image: true, error}, `${stateName || idx}_${retryIndex}`); - }); - - return this._shouldAddErrorTab(activeResult) - ? tabs.concat(this._drawTab(activeResult, {errorDetails})) - : tabs; - } - - _drawTab(state, opts = {}, key = '') { - const {result: {name: browserId}, suite: {suitePath}} = this.props; - - opts = defaults({error: state.error}, opts); - - return ( -
-
- -
-
- ); - } - - _shouldAddErrorTab({multipleTabs, status, screenshot}) { - return multipleTabs && isErroredStatus(status) && !screenshot; + _getActiveResultId = () => { + return this.props.resultIds[this.state.retry]; } render() { - const activeResult = this._getActiveResult(); - const {metaInfo, suiteUrl, description} = activeResult; + const {testName} = this.props; + const activeResultId = this._getActiveResultId(); return (
@@ -192,9 +105,7 @@ class Body extends Component { {this._addRetrySwitcher()} {this._addRetryButton()}
- - {description && } - {this._getTabs()} +
); @@ -202,12 +113,6 @@ class Body extends Component { } export default connect( - ({reporter: {gui, running, suites, suiteIds, apiValues, view: {retryIndex}}}) => ({ - failed: values(pick(suites, suiteIds.failed)), - gui, - running, - apiValues, - retryIndex - }), + ({gui, running, view: {retryIndex}}) => ({gui, running, retryIndex}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(Body); diff --git a/lib/static/components/section/body/meta-info.js b/lib/static/components/section/body/meta-info.js index 6b26dac9b..d63635c39 100644 --- a/lib/static/components/section/body/meta-info.js +++ b/lib/static/components/section/body/meta-info.js @@ -1,11 +1,9 @@ -'use strict'; - import path from 'path'; import url from 'url'; import React, {Component} from 'react'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; -import {map, mapValues, isObject} from 'lodash'; +import {map, mapValues, isObject, omitBy, isEmpty} from 'lodash'; import Details from '../../details'; import {isUrl} from '../../../modules/utils'; @@ -31,20 +29,37 @@ const metaToElements = (metaInfo, metaInfoBaseUrls) => { class MetaInfo extends Component { static propTypes = { - metaInfo: PropTypes.object.isRequired, - suiteUrl: PropTypes.string.isRequired, - getExtraMetaInfo: PropTypes.func.isRequired, - metaInfoBaseUrls: PropTypes.object.isRequired + result: PropTypes.shape({ + metaInfo: PropTypes.object.isRequired, + suiteUrl: PropTypes.string.isRequired + }).isRequired, + testName: PropTypes.string.isRequired, + // from store + metaInfoBaseUrls: PropTypes.object.isRequired, + apiValues: PropTypes.shape({ + extraItems: PropTypes.object.isRequired, + metaInfoExtenders: PropTypes.object.isRequired + }).isRequired }; + getExtraMetaInfo = () => { + const {testName, apiValues: {extraItems, metaInfoExtenders}} = this.props; + + return omitBy(mapValues(metaInfoExtenders, (extender) => { + const stringifiedFn = extender.startsWith('return') ? extender : `return ${extender}`; + + return new Function(stringifiedFn)()({testName}, extraItems); + }), isEmpty); + } + render() { - const {metaInfo, suiteUrl, getExtraMetaInfo, metaInfoBaseUrls} = this.props; - const serializedMetaValues = serializeMetaValues(metaInfo); - const extraMetaInfo = getExtraMetaInfo(); + const {result, metaInfoBaseUrls} = this.props; + const serializedMetaValues = serializeMetaValues(result.metaInfo); + const extraMetaInfo = this.getExtraMetaInfo(); const formattedMetaInfo = { ...serializedMetaValues, ...extraMetaInfo, - url: mkLinkToUrl(suiteUrl, metaInfo.url) + url: mkLinkToUrl(result.suiteUrl, result.metaInfo.url) }; const metaElements = metaToElements(formattedMetaInfo, metaInfoBaseUrls); @@ -54,7 +69,5 @@ class MetaInfo extends Component { } export default connect( - ({reporter: state}) => ({ - metaInfoBaseUrls: state.config.metaInfoBaseUrls - }) + ({config: {metaInfoBaseUrls}, apiValues}) => ({metaInfoBaseUrls, apiValues}) )(MetaInfo); diff --git a/lib/static/components/section/body/result.js b/lib/static/components/section/body/result.js new file mode 100644 index 000000000..82771ded2 --- /dev/null +++ b/lib/static/components/section/body/result.js @@ -0,0 +1,35 @@ +import React, {Component, Fragment} from 'react'; +import {connect} from 'react-redux'; +import PropTypes from 'prop-types'; +import MetaInfo from './meta-info'; +import Description from './description'; +import Tabs from './tabs'; + +class Result extends Component { + static propTypes = { + resultId: PropTypes.string.isRequired, + testName: PropTypes.string.isRequired, + // from store + result: PropTypes.shape({ + status: PropTypes.string.isRequired, + imageIds: PropTypes.array.isRequired, + description: PropTypes.string + }).isRequired + } + + render() { + const {result, testName} = this.props; + + return ( + + + {result.description && } + + + ); + } +} + +export default connect( + ({tree}, {resultId}) => ({result: tree.results.byId[resultId]}) +)(Result); diff --git a/lib/static/components/section/body/retry-switcher/index.js b/lib/static/components/section/body/retry-switcher/index.js new file mode 100644 index 000000000..afcb06006 --- /dev/null +++ b/lib/static/components/section/body/retry-switcher/index.js @@ -0,0 +1,35 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import RetrySwitcherItem from './item'; + +export default class RetrySwitcher extends Component { + static propTypes = { + resultIds: PropTypes.arrayOf(PropTypes.string).isRequired, + retryIndex: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired + } + + render() { + const {resultIds, retryIndex, onChange} = this.props; + + if (resultIds.length <= 1) { + return null; + } + + return ( +
+ {resultIds.map((resultId, ind) => { + const isActive = ind === retryIndex; + + return onChange(ind)}> + {ind + 1} + ; + })} +
+ ); + } +} diff --git a/lib/static/components/section/body/retry-switcher/item.js b/lib/static/components/section/body/retry-switcher/item.js new file mode 100644 index 000000000..32c81c698 --- /dev/null +++ b/lib/static/components/section/body/retry-switcher/item.js @@ -0,0 +1,35 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import {connect} from 'react-redux'; + +class RetrySwitcherItem extends Component { + static propTypes = { + resultId: PropTypes.string.isRequired, + isActive: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + // from store + status: PropTypes.string.isRequired + } + + render() { + const {status, isActive, onClick, children} = this.props; + + const className = classNames( + 'state-button', + 'tab-switcher__button', + {[`tab-switcher__button_status_${status}`]: status}, + {'tab-switcher__button_active': isActive} + ); + + return ; + } +} + +export default connect( + ({tree}, {resultId}) => { + const result = tree.results.byId[resultId]; + + return {status: result.status}; + } +)(RetrySwitcherItem); diff --git a/lib/static/components/section/body/tabs.js b/lib/static/components/section/body/tabs.js new file mode 100644 index 000000000..287588f66 --- /dev/null +++ b/lib/static/components/section/body/tabs.js @@ -0,0 +1,52 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import {isEmpty} from 'lodash'; +import State from '../../state'; +import {isSuccessStatus, isErroredStatus} from '../../../../common-utils'; + +export default class Tabs extends Component { + static propTypes = { + result: PropTypes.shape({ + id: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + imageIds: PropTypes.array.isRequired, + multipleTabs: PropTypes.bool.isRequired, + screenshot: PropTypes.bool.isRequired + }).isRequired + } + + _shouldAddErrorTab() { + const {result} = this.props; + + return result.multipleTabs && isErroredStatus(result.status) && !result.screenshot; + } + + _drawTab({key, imageId = null}) { + const {result} = this.props; + + return ( +
+
+ +
+
+ ); + } + + render() { + const {result} = this.props; + const errorTabId = `${result.id}_error`; + + if (isEmpty(result.imageIds)) { + return isSuccessStatus(result.status) + ? null + : this._drawTab({key: errorTabId}); + } + + const tabs = result.imageIds.map((imageId) => this._drawTab({key: imageId, imageId})); + + return this._shouldAddErrorTab() + ? tabs.concat(this._drawTab({key: errorTabId})) + : tabs; + } +} diff --git a/lib/static/components/section/section-browser.js b/lib/static/components/section/section-browser.js index 1eb97639d..ed67a6b3e 100644 --- a/lib/static/components/section/section-browser.js +++ b/lib/static/components/section/section-browser.js @@ -1,27 +1,39 @@ -'use strict'; - import React, {Component, Fragment} from 'react'; -import {bindActionCreators} from 'redux'; +import {last} from 'lodash'; import {connect} from 'react-redux'; import Parser from 'html-react-parser'; -import * as actions from '../../modules/actions'; import PropTypes from 'prop-types'; import SectionWrapper from './section-wrapper'; import BrowserTitle from './title/browser'; import BrowserSkippedTitle from './title/browser-skipped'; import Body from './body'; -import {isFailStatus, isErroredStatus, isSkippedStatus} from '../../../common-utils'; +import {isSkippedStatus} from '../../../common-utils'; +import {isNodeFailed} from '../../modules/utils'; +import {mkShouldBrowserBeShown, mkHasBrowserFailedRetries} from '../../modules/selectors/tree'; class SectionBrowser extends Component { static propTypes = { + browserId: PropTypes.string.isRequired, + errorGroupBrowserIds: PropTypes.array, + // from store + expand: PropTypes.string.isRequired, browser: PropTypes.shape({ name: PropTypes.string.isRequired, - result: PropTypes.object.isRequired, - retries: PropTypes.array - }), - suite: PropTypes.object, - shouldBeOpened: PropTypes.func, - sectionStatusResolver: PropTypes.func + resultIds: PropTypes.arrayOf(PropTypes.string).isRequired, + parentId: PropTypes.string.isRequired + }).isRequired, + lastResult: PropTypes.shape({ + id: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + error: PropTypes.object, + imageIds: PropTypes.arrayOf(PropTypes.string).isRequired, + skipReason: PropTypes.string + }).isRequired, + shouldBeShown: PropTypes.bool.isRequired, + hasFailedRetries: PropTypes.bool.isRequired, + // from SectionCommonWrapper + shouldBeOpened: PropTypes.func.isRequired, + sectionStatusResolver: PropTypes.func.isRequired } state = { @@ -37,52 +49,70 @@ class SectionBrowser extends Component { this.setState({opened: !this.state.opened}); } - _getStates() { - const {expand, browser} = this.props; - const {result: {status}, retries = []} = browser; - const failed = isErroredStatus(status) || isFailStatus(status); - const retried = retries.length > 0; + _getStates(props = this.props) { + const {expand, lastResult, hasFailedRetries} = props; + const failed = isNodeFailed(lastResult); + const retried = hasFailedRetries; return {failed, retried, expand}; } - _generateSkippedTitle() { - const {name, result: {skipReason}} = this.props.browser; - return - [skipped] {name} - {skipReason && ', reason: '} - {skipReason && Parser(skipReason)} - ; + _generateSkippedTitle(skipReason) { + return ( + + [skipped] {this.props.browser.name} + {skipReason && ', reason: '} + {skipReason && Parser(skipReason)} + + ); } render() { - const {browser, suite} = this.props; - const {name, result, retries = [], result: {status}} = browser; + if (!this.props.shouldBeShown) { + return null; + } + + const {browser, lastResult} = this.props; const {opened} = this.state; - const skipped = isSkippedStatus(status); - const retried = retries.length > 0; - const title = skipped - ? this._generateSkippedTitle() - : name; + const isSkippedLastResult = isSkippedStatus(lastResult.status); + const hasRetries = browser.resultIds.length > 1; - const body = opened - ? - : null; + const title = isSkippedLastResult + ? this._generateSkippedTitle(lastResult.skipReason) + : browser.name; // Detect executed test but failed and skipped - const executed = retried || result.error || (result.imagesInfo && result.imagesInfo.length > 0); - const section = skipped && !executed + const isExecutedResult = hasRetries || lastResult.error || lastResult.imageIds.length > 0; + const isSkipped = isSkippedLastResult && !isExecutedResult; + + const body = isSkipped || !opened + ? null + : ( + + ); + + const section = isSkipped ? : ( - + {body} ); return ( -
+
{section}
); @@ -90,6 +120,27 @@ class SectionBrowser extends Component { } export default connect( - ({reporter: {view: {expand}}}) => ({expand}), - (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) + () => { + const hasBrowserFailedRetries = mkHasBrowserFailedRetries(); + const shouldBrowserBeShown = mkShouldBrowserBeShown(); + + return (state, {browserId, errorGroupBrowserIds}) => { + const browser = state.tree.browsers.byId[browserId]; + const lastResult = state.tree.results.byId[last(browser.resultIds)]; + const shouldBeShown = shouldBrowserBeShown(state, {browserId, result: lastResult, errorGroupBrowserIds}); + let hasFailedRetries = false; + + if (shouldBeShown) { + hasFailedRetries = hasBrowserFailedRetries(state, {browserId}); + } + + return { + expand: state.view.expand, + browser, + lastResult, + shouldBeShown, + hasFailedRetries + }; + }; + }, )(SectionWrapper(SectionBrowser)); diff --git a/lib/static/components/section/section-common.js b/lib/static/components/section/section-common.js index cdeb3bca5..ec55cc844 100644 --- a/lib/static/components/section/section-common.js +++ b/lib/static/components/section/section-common.js @@ -1,35 +1,39 @@ -'use strict'; - import React, {Component} from 'react'; -import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import * as actions from '../../modules/actions'; import PropTypes from 'prop-types'; -import {uniqueId} from 'lodash'; +import LazilyRender from '@gemini-testing/react-lazily-render'; import SectionWrapper from './section-wrapper'; import SectionBrowser from './section-browser'; -import {hasFails, hasFailedRetries, shouldSuiteBeShown, shouldBrowserBeShown} from '../../modules/utils'; +import {isFailStatus, isErroredStatus} from '../../../common-utils'; import Title from './title/simple'; +import {mkShouldSuiteBeShown, mkHasSuiteFailedRetries} from '../../modules/selectors/tree'; class SectionCommon extends Component { static propTypes = { - suite: PropTypes.shape({ - name: PropTypes.string, - suitePath: PropTypes.array, - browsers: PropTypes.array, - children: PropTypes.array - }), - testNameFilter: PropTypes.string, - strictMatchFilter: PropTypes.bool, - filteredBrowsers: PropTypes.array, - errorGroupTests: PropTypes.object, - shouldBeOpened: PropTypes.func, - sectionStatusResolver: PropTypes.func, - eventToUpdate: PropTypes.string + suiteId: PropTypes.string.isRequired, + eventToUpdate: PropTypes.string, + errorGroupBrowserIds: PropTypes.array.isRequired, + sectionRoot: PropTypes.bool.isRequired, + // from store + expand: PropTypes.string.isRequired, + suiteName: PropTypes.string.isRequired, + suiteStatus: PropTypes.string.isRequired, + suiteChildIds: PropTypes.array, + suiteBrowserIds: PropTypes.array, + shouldBeShown: PropTypes.bool.isRequired, + hasFailedRetries: PropTypes.bool.isRequired, + // from SectionCommonWrapper + shouldBeOpened: PropTypes.func.isRequired, + sectionStatusResolver: PropTypes.func.isRequired } state = { - opened: this.props.shouldBeOpened(this._getStates()) + opened: this.props.shouldBeOpened(this._getStates()), + isRendered: false + } + + handleRender = () => { + this.setState({isRendered: true}); } componentWillReceiveProps(nextProps) { @@ -38,11 +42,11 @@ class SectionCommon extends Component { } _getStates(props = this.props) { - const {suite, expand} = props; + const {suiteStatus, expand, hasFailedRetries} = props; return { - failed: hasFails(suite), - retried: hasFailedRetries(suite), + failed: isFailStatus(suiteStatus) || isErroredStatus(suiteStatus), + retried: hasFailedRetries, expand }; } @@ -56,78 +60,97 @@ class SectionCommon extends Component { } } - render() { + _drawSection() { const { - suite, - testNameFilter, - strictMatchFilter, - filteredBrowsers, - sectionStatusResolver, - errorGroupTests, - viewMode, - sectionRoot = true + suiteId, + suiteName, + suiteStatus, + suiteChildIds, + suiteBrowserIds, + errorGroupBrowserIds, + sectionRoot, + sectionStatusResolver } = this.props; const {opened} = this.state; - const { - name, - browsers = [], - children = [], - status, - suitePath - } = suite; - const fullTestName = suitePath.join(' '); if (!opened) { return ( -
- + <div className={sectionStatusResolver({status: suiteStatus, opened, sectionRoot})}> + <Title name={suiteName} suiteId={suiteId} handler={this._onToggleSection} /> </div> ); } - const visibleChildren = children.filter(child => shouldSuiteBeShown({suite: child, testNameFilter, strictMatchFilter, filteredBrowsers, errorGroupTests, viewMode})); - - const childrenTmpl = visibleChildren.map((child) => { - const key = uniqueId(`${suite.suitePath}-${child.name}`); - + const childSuitesTmpl = suiteChildIds && suiteChildIds.map((suiteId) => { return <SectionCommonWrapper - key={key} - suite={child} - testNameFilter={testNameFilter} - strictMatchFilter={strictMatchFilter} - filteredBrowsers={filteredBrowsers} - errorGroupTests={errorGroupTests} + key={suiteId} + suiteId={suiteId} sectionRoot={false} + errorGroupBrowserIds={errorGroupBrowserIds} />; }); - const browserTmpl = browsers - .filter(browser => shouldBrowserBeShown({browser, fullTestName, filteredBrowsers, errorGroupTests, viewMode})) - .map(browser => { - return ( - <SectionBrowser - key={browser.name} - browser={browser} - suite={suite} - /> - ); - }); + const browsersTmpl = suiteBrowserIds && suiteBrowserIds.map((browserId) => { + return <SectionBrowser key={browserId} browserId={browserId} errorGroupBrowserIds={errorGroupBrowserIds} />; + }); return ( - <div className={sectionStatusResolver({status, opened, sectionRoot})}> - <Title name={name} suite={suite} handler={this._onToggleSection} /> + <div className={sectionStatusResolver({status: suiteStatus, opened, sectionRoot})}> + <Title name={suiteName} suiteId={suiteId} handler={this._onToggleSection} /> <div className="section__body"> - {browserTmpl} - {childrenTmpl} + {childSuitesTmpl} + {browsersTmpl} </div> </div> ); } + + _drawLazySection() { + const {suiteId, eventToUpdate, lazyLoadOffset} = this.props; + const content = this.state.isRendered ? this._drawSection() : null; + + return <LazilyRender key={suiteId} eventToUpdate={eventToUpdate} offset={lazyLoadOffset} onRender={this.handleRender} content={content} />; + } + + render() { + if (!this.props.shouldBeShown) { + return null; + } + + const {sectionRoot, lazyLoadOffset} = this.props; + + return sectionRoot && lazyLoadOffset > 0 + ? this._drawLazySection() + : this._drawSection(); + } } const SectionCommonWrapper = connect( - ({reporter: {view: {expand, viewMode}}}) => ({expand, viewMode}), - (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) + () => { + const shouldSuiteBeShown = mkShouldSuiteBeShown(); + const hasSuiteFailedRetries = mkHasSuiteFailedRetries(); + + return (state, {suiteId, errorGroupBrowserIds, sectionRoot}) => { + const suite = state.tree.suites.byId[suiteId]; + const shouldBeShown = sectionRoot ? true : shouldSuiteBeShown(state, {suiteId, errorGroupBrowserIds}); + let hasFailedRetries = false; + + if (shouldBeShown) { + hasFailedRetries = hasSuiteFailedRetries(state, {suiteId}); + } + + return { + expand: state.view.expand, + lazyLoadOffset: state.view.lazyLoadOffset, + suiteName: suite.name, + suiteStatus: suite.status, + suiteChildIds: suite.suiteIds, + suiteBrowserIds: suite.browserIds, + shouldBeShown, + hasFailedRetries + }; + }; + }, )(SectionWrapper(SectionCommon)); export default SectionCommonWrapper; diff --git a/lib/static/components/section/switcher-retry.js b/lib/static/components/section/switcher-retry.js deleted file mode 100644 index c43be472e..000000000 --- a/lib/static/components/section/switcher-retry.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class SwitcherRetry extends Component { - static propTypes = { - testResults: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, - retryIndex: PropTypes.number.isRequired - } - - constructor(props) { - super(props); - this.state = {retry: this.props.retryIndex}; - } - - render() { - const {testResults} = this.props; - - if (testResults.length <= 1) { - return null; - } - - const buttonsTmpl = []; - - for (let i = 0; i < testResults.length; i++) { - const currStatus = testResults[i].status; - const className = classNames( - 'state-button', - 'tab-switcher__button', - {[`tab-switcher__button_status_${currStatus}`]: currStatus}, - {'tab-switcher__button_active': i === this.state.retry} - ); - buttonsTmpl.push( - <button key={i} className={className} onClick={() => this._onChange(i)}>{i + 1}</button> - ); - } - - return (<div className="tab-switcher">{buttonsTmpl}</div>); - } - - _onChange(index) { - this.setState({retry: index}); - this.props.onChange(index); - } -} diff --git a/lib/static/components/section/title/browser.js b/lib/static/components/section/title/browser.js index 715a191cd..ee3d0b97b 100644 --- a/lib/static/components/section/title/browser.js +++ b/lib/static/components/section/title/browser.js @@ -1,31 +1,54 @@ -'use strict'; - import url from 'url'; import React, {Component} from 'react'; import ClipboardButton from 'react-clipboard.js'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; +import {get} from 'lodash'; import {appendQuery} from '../../../modules/query-params'; import viewModes from '../../../../constants/view-modes'; class BrowserTitle extends Component { static propTypes = { title: PropTypes.node.isRequired, - result: PropTypes.object.isRequired, - suite: PropTypes.object.isRequired, - browser: PropTypes.object.isRequired, + browserId: PropTypes.string.isRequired, + lastResultId: PropTypes.string.isRequired, handler: PropTypes.func.isRequired, + // from store + testName: PropTypes.string.isRequired, + retryIndex: PropTypes.number.isRequired, + suiteUrl: PropTypes.string, parsedHost: PropTypes.object } + _buildUrl(href, host) { + return host + ? url.format(Object.assign(url.parse(href), host)) + : href; + } + + _getTestUrl() { + const {title, testName, retryIndex} = this.props; + + return appendQuery(window.location.href, { + browser: title, + testNameFilter: testName, + strictMatchFilter: true, + retryIndex, + viewModes: viewModes.ALL, + expand: 'all', + groupByError: false + }); + } + render() { - const {title, result, handler, parsedHost} = this.props; + const {title, suiteUrl, handler, parsedHost} = this.props; + return ( <div className="section__title section__title_type_browser" onClick={handler}> {title} <a className="button section__icon section__icon_view-local" - href={this._buildUrl(result.suiteUrl, parsedHost)} + href={this._buildUrl(suiteUrl, parsedHost)} onClick={(e) => e.stopPropagation()} title="view in browser" target="_blank"> @@ -39,29 +62,19 @@ class BrowserTitle extends Component { </div> ); } - - _buildUrl(href, host) { - return host - ? url.format(Object.assign(url.parse(href), host)) - : href; - } - - _getTestUrl() { - const {browser, suite} = this.props; - - return appendQuery(window.location.href, { - browser: browser.name, - testNameFilter: suite.suitePath.join(' '), - strictMatchFilter: true, - retryIndex: browser.state.retryIndex, - viewModes: viewModes.ALL, - expand: 'all', - groupByError: false - }); - } } export default connect( - ({reporter: state}) => ({parsedHost: state.view.parsedHost}), - null + ({tree, view}, {browserId, lastResultId}) => { + const browser = tree.browsers.byId[browserId]; + const browserState = tree.browsers.stateById[browserId]; + const lastResult = tree.results.byId[lastResultId]; + + return { + testName: browser.parentId, + retryIndex: get(browserState, 'retryIndex', 0), + suiteUrl: lastResult.suiteUrl, + parsedHost: view.parsedHost + }; + } )(BrowserTitle); diff --git a/lib/static/components/section/title/simple.js b/lib/static/components/section/title/simple.js index 0a869e5b1..35481f6a5 100644 --- a/lib/static/components/section/title/simple.js +++ b/lib/static/components/section/title/simple.js @@ -2,24 +2,29 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; import PropTypes from 'prop-types'; import ClipboardButton from 'react-clipboard.js'; -import {retrySuite} from '../../../modules/actions'; +import * as actions from '../../../modules/actions'; +import {mkGetTestsBySuiteId} from '../../../modules/selectors/tree'; class SectionTitle extends Component { static propTypes = { name: PropTypes.string.isRequired, - suite: PropTypes.shape({ - suitePath: PropTypes.array - }).isRequired, + suiteId: PropTypes.string.isRequired, handler: PropTypes.func.isRequired, - gui: PropTypes.bool + // from store + gui: PropTypes.bool.isRequired, + suiteTests: PropTypes.arrayOf(PropTypes.shape({ + testName: PropTypes.string, + browserName: PropTypes.string + })).isRequired } onSuiteRetry = (e) => { e.stopPropagation(); - this.props.retrySuite(this.props.suite); + this.props.actions.retrySuite(this.props.suiteTests); } render() { @@ -40,7 +45,7 @@ class SectionTitle extends Component { onClick={(e) => e.stopPropagation()} className="button section__icon section__icon_copy-to-clipboard" button-title="copy to clipboard" - data-clipboard-text={this.props.suite.suitePath.join(' ')}> + data-clipboard-text={this.props.suiteId}> </ClipboardButton> ); } @@ -56,4 +61,16 @@ class SectionTitle extends Component { } } -export default connect(({reporter: {gui}}) => ({gui}), {retrySuite})(SectionTitle); +export default connect( + () => { + const getTestsBySuiteId = mkGetTestsBySuiteId(); + + return (state, {suiteId}) => { + return { + gui: state.gui, + suiteTests: getTestsBySuiteId(state, {suiteId}) + }; + }; + }, + (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) +)(SectionTitle); diff --git a/lib/static/components/skipped-list.js b/lib/static/components/skipped-list.js index 9d5337817..570b0762a 100644 --- a/lib/static/components/skipped-list.js +++ b/lib/static/components/skipped-list.js @@ -39,7 +39,7 @@ class SkippedList extends Component { } export default connect( - ({reporter: state}) => ({ + (state) => ({ showSkipped: state.view.showSkipped, skips: state.skips }) diff --git a/lib/static/components/state/index.js b/lib/static/components/state/index.js index 29210b5a6..421036a16 100644 --- a/lib/static/components/state/index.js +++ b/lib/static/components/state/index.js @@ -3,41 +3,49 @@ import {get} from 'lodash'; import React, {Component, Fragment} from 'react'; import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import * as actions from '../../modules/actions'; import StateError from './state-error'; import StateSuccess from './state-success'; import StateFail from './state-fail'; import ControlButton from '../controls/control-button'; -import {isAcceptable} from '../../modules/utils'; +import FindSameDiffsButton from '../controls/find-same-diffs-button'; +import {isAcceptable, isNodeFailed} from '../../modules/utils'; import {isSuccessStatus, isFailStatus, isErroredStatus, isUpdatedStatus, isIdleStatus} from '../../../common-utils'; class State extends Component { static propTypes = { - state: PropTypes.shape({ + result: PropTypes.shape({ + status: PropTypes.string.isRequired, + error: PropTypes.object + }).isRequired, + imageId: PropTypes.string, + // from store + gui: PropTypes.bool.isRequired, + expand: PropTypes.string.isRequired, + scaleImages: PropTypes.bool.isRequired, + closeIds: PropTypes.array, + image: PropTypes.shape({ status: PropTypes.string, - expectedImg: PropTypes.object, - actualImg: PropTypes.object, - diffImg: PropTypes.object - }), - suitePath: PropTypes.array, - browserId: PropTypes.string, - image: PropTypes.bool, - error: PropTypes.object, - gui: PropTypes.bool, - scaleImages: PropTypes.bool, - expand: PropTypes.string, - acceptHandler: PropTypes.func, - findSameDiffsHandler: PropTypes.func, - toggleHandler: PropTypes.func + error: PropTypes.object, + stateName: PropTypes.string, + expectedImg: PropTypes.object + }).isRequired, + imageOpened: PropTypes.bool } + static defaultProps = { + image: {} + }; + constructor(props) { super(props); - const {state: {stateName}, toggleHandler} = this.props; - toggleHandler({stateName, opened: this._shouldBeOpened()}); - this.state = {modalOpen: false, opened: stateName ? this.props.state.opened : true}; + const opened = this.props.image.stateName ? this._shouldBeOpened() : true; + this.state = {opened}; + this.onToggleStateResult({opened}); } componentWillReceiveProps(nextProps) { @@ -45,21 +53,50 @@ class State extends Component { return; } - const {suitePath, browserId, state: {stateName}, toggleHandler} = this.props; - const fullTitle = suitePath.concat([browserId, stateName]).join(' '); - const opened = !nextProps.closeIds.includes(fullTitle); + if (nextProps.expand !== this.props.expand) { + const opened = this._shouldBeOpened(nextProps); + + if (this.state.opened !== opened) { + this.setState({opened}); + this.onToggleStateResult({opened}); + } + } + + const {imageId} = this.props; + const opened = !nextProps.closeIds.includes(imageId); if (opened !== this.state.opened) { - toggleHandler({stateName, opened}); this.setState({opened}); + this.onToggleStateResult({opened}); } } - _shouldBeOpened() { - const {expand} = this.props; - const {status} = this.props.state; + componentWillUnmount() { + this.onToggleStateResult({opened: false}); + } + + onToggleStateResult = ({opened}) => { + const {imageId} = this.props; + + if (!imageId) { + return; + } + + this.props.actions.toggleStateResult({imageId, opened}); + } + + onTestAccept = () => { + this.props.actions.acceptTest(this.props.imageId); + } + + _initOpened() { + return this.props.image.stateName ? this._shouldBeOpened() : true; + } - if ((expand === 'errors' || expand === 'retries') && (isFailStatus(status) || isErroredStatus(status))) { + _shouldBeOpened(props = this.props) { + const {expand, node} = props; + + if ((expand === 'errors' || expand === 'retries') && isNodeFailed(node)) { return true; } else if (expand === 'all') { return true; @@ -73,9 +110,9 @@ class State extends Component { return null; } - const {state, state: {stateName}, acceptHandler, findSameDiffsHandler} = this.props; - const isAcceptDisabled = !isAcceptable(state); - const isFindSameDiffDisabled = !isFailStatus(state.status); + const {node, imageId, result} = this.props; + const isAcceptDisabled = !isAcceptable(node); + const isFindSameDiffDisabled = !isFailStatus(node.status); return ( <div className="state-controls"> @@ -83,66 +120,60 @@ class State extends Component { label="✔ Accept" isSuiteControl={true} isDisabled={isAcceptDisabled} - handler={() => acceptHandler(stateName)} + handler={this.onTestAccept} /> - <ControlButton - label="🔍 Find same diffs" - isSuiteControl={true} + <FindSameDiffsButton + imageId={imageId} + browserId={result.parentId} isDisabled={isFindSameDiffDisabled} - handler={() => { - findSameDiffsHandler(stateName); - this.setState({modalOpen: true}); - }} /> </div> ); } _toggleState = () => { - const {state: {stateName, opened}, toggleHandler} = this.props; - - toggleHandler({stateName, opened: !opened}); this.setState({opened: !this.state.opened}); + this.onToggleStateResult({opened: !this.props.imageOpened}); } - _getStateTitle(stateName, status) { + _getStateTitle() { + const {image, imageOpened} = this.props; + + if (!image.stateName) { + return null; + } + const className = classNames( 'state-title', - {'state-title_collapsed': !this.state.opened}, - `state-title_${status}` + {'state-title_collapsed': !imageOpened}, + `state-title_${image.status}` ); - return stateName - ? <div className={className} onClick={this._toggleState}>{stateName}</div> - : null; - } - - _getErrorPattern(error) { - return this.props.config.errorPatterns.find(({regexp}) => error.message.match(regexp)); + return <div className={className} onClick={this._toggleState}>{image.stateName}</div>; } render() { - const {status, expectedImg, actualImg, diffImg, stateName, diffClusters} = this.props.state; - const {image, error, errorDetails} = this.props; - let elem = null; - if (!this.state.opened) { return ( <Fragment> <hr className="tab__separator" /> - {this._getStateTitle(stateName, status)} + {this._getStateTitle()} </Fragment> ); } + const {node, result, image} = this.props; + const {status, error} = node; + let elem = null; + if (isErroredStatus(status)) { - elem = <StateError image={Boolean(image)} actualImg={actualImg} error={error} errorPattern={this._getErrorPattern(error)} errorDetails={errorDetails}/>; - } else if (isSuccessStatus(status) || isUpdatedStatus(status) || (isIdleStatus(status) && get(expectedImg, 'path'))) { - elem = <StateSuccess status={status} expectedImg={expectedImg} />; + elem = <StateError result={result} image={image} />; + } else if (isSuccessStatus(status) || isUpdatedStatus(status) || (isIdleStatus(status) && get(image.expectedImg, 'path'))) { + elem = <StateSuccess status={status} expectedImg={image.expectedImg} />; } else if (isFailStatus(status)) { elem = error - ? <StateError image={Boolean(image)} actualImg={actualImg} error={error} errorPattern={this._getErrorPattern(error)} errorDetails={errorDetails}/> - : <StateFail expectedImg={expectedImg} actualImg={actualImg} diffImg={diffImg} diffClusters={diffClusters}/>; + ? <StateError result={result} image={image} /> + : <StateFail image={image} />; } const className = classNames( @@ -153,7 +184,7 @@ class State extends Component { return ( <Fragment> <hr className="tab__separator"/> - {this._getStateTitle(stateName, status)} + {this._getStateTitle()} {this._drawControlButtons()} {elem ? <div className={className}>{elem}</div> : null} </Fragment> @@ -162,5 +193,20 @@ class State extends Component { } export default connect( - ({reporter: {gui, view: {expand, scaleImages}, closeIds, config}}) => ({gui, expand, scaleImages, closeIds, config}) + ({gui, tree, view: {expand, scaleImages}, closeIds}, {imageId, result}) => { + const image = tree.images.byId[imageId]; + const {opened = true} = tree.images.stateById[imageId] || {}; + const node = imageId ? image : result; + + return { + gui, + expand, + scaleImages, + closeIds, + image, + node, + imageOpened: opened + }; + }, + (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(State); diff --git a/lib/static/components/state/screenshot.js b/lib/static/components/state/screenshot.js index cb12cb61d..409675c63 100644 --- a/lib/static/components/state/screenshot.js +++ b/lib/static/components/state/screenshot.js @@ -104,4 +104,4 @@ function addTimestamp(imagePath) { return `${imagePath}?t=${Date.now()}`; } -export default connect(({reporter: {view: {lazyLoadOffset}}}) => ({lazyLoadOffset}))(Screenshot); +export default connect(({view: {lazyLoadOffset}}) => ({lazyLoadOffset}))(Screenshot); diff --git a/lib/static/components/state/state-error.js b/lib/static/components/state/state-error.js index 01cea98cf..439408e14 100644 --- a/lib/static/components/state/state-error.js +++ b/lib/static/components/state/state-error.js @@ -1,6 +1,7 @@ 'use strict'; import React, {Component, Fragment} from 'react'; +import {connect} from 'react-redux'; import PropTypes from 'prop-types'; import {isEmpty, map} from 'lodash'; import ReactHtmlParser from 'react-html-parser'; @@ -10,37 +11,39 @@ import ErrorDetails from './error-details'; import Details from '../details'; import {ERROR_TITLE_TEXT_LENGTH} from '../../../constants/errors'; -export default class StateError extends Component { +class StateError extends Component { static propTypes = { - image: PropTypes.bool.isRequired, + result: PropTypes.shape({ + error: PropTypes.object, + errorDetails: PropTypes.object + }).isRequired, + image: PropTypes.shape({ + stateName: PropTypes.string, + error: PropTypes.object, + actualImg: PropTypes.object + }).isRequired, + // from store error: PropTypes.object.isRequired, - actualImg: PropTypes.object, - errorPattern: PropTypes.object + errorDetails: PropTypes.object, + errorPatterns: PropTypes.array.isRequired }; - render() { - const {image, error, errorDetails, actualImg, errorPattern = {}} = this.props; - const extendedError = isEmpty(errorPattern) - ? error - : {...error, message: `${errorPattern.name}\n${error.message}`, hint: parseHtmlString(errorPattern.hint)}; + _getErrorPattern() { + const {errorPatterns, error} = this.props; - return ( - <div className="image-box__image image-box__image_single"> - <div className="error">{this._errorToElements(extendedError)}</div> - {errorDetails && <ErrorDetails errorDetails={errorDetails}/>} - {this._drawImage(image, actualImg)} - </div> - ); + return errorPatterns.find(({regexp}) => error.message.match(regexp)); } - _drawImage(image, actualImg) { - if (!image) { + _drawImage() { + const {image, error} = this.props; + + if (!image.actualImg) { return null; } - return isNoRefImageError(this.props.error) - ? <Screenshot image={actualImg} /> - : <Details title="Page screenshot" content={<Screenshot image={actualImg} noLazyLoad={true} />} extendClassNames="details_type_image" />; + return isNoRefImageError(error) + ? <Screenshot image={image.actualImg} /> + : <Details title="Page screenshot" content={<Screenshot image={image.actualImg} noLazyLoad={true} />} extendClassNames="details_type_image" />; } _errorToElements(error) { @@ -79,8 +82,34 @@ export default class StateError extends Component { />; }); } + + render() { + const {error, errorDetails} = this.props; + const errorPattern = this._getErrorPattern(); + + const extendedError = isEmpty(errorPattern) + ? error + : {...error, message: `${errorPattern.name}\n${error.message}`, hint: parseHtmlString(errorPattern.hint)}; + + return ( + <div className="image-box__image image-box__image_single"> + <div className="error">{this._errorToElements(extendedError)}</div> + {errorDetails && <ErrorDetails errorDetails={errorDetails} />} + {this._drawImage()} + </div> + ); + } } +export default connect( + ({config: {errorPatterns}}, {result, image}) => { + const error = image.error || result.error; + const errorDetails = image.stateName ? null : result.errorDetails; + + return {error, errorDetails, errorPatterns}; + } +)(StateError); + function parseHtmlString(str = '') { const html = str ? ReactHtmlParser(str) : null; diff --git a/lib/static/components/state/state-fail.js b/lib/static/components/state/state-fail.js index 9006de3d9..1f7ea856d 100644 --- a/lib/static/components/state/state-fail.js +++ b/lib/static/components/state/state-fail.js @@ -7,15 +7,18 @@ import Screenshot from './screenshot'; class StateFail extends Component { static propTypes = { - expectedImg: PropTypes.object.isRequired, - actualImg: PropTypes.object.isRequired, - diffImg: PropTypes.object.isRequired, - showOnlyDiff: PropTypes.bool.isRequired, - diffClusters: PropTypes.array + image: PropTypes.shape({ + expectedImg: PropTypes.object.isRequired, + actualImg: PropTypes.object.isRequired, + diffImg: PropTypes.object.isRequired, + diffClusters: PropTypes.array + }).isRequired, + // from store + showOnlyDiff: PropTypes.bool.isRequired } render() { - const {expectedImg, actualImg, diffImg, diffClusters} = this.props; + const {image: {expectedImg, actualImg, diffImg, diffClusters}} = this.props; return ( <Fragment> @@ -38,14 +41,16 @@ class StateFail extends Component { ); } - _drawImageBox(label, image, diffClusters) { + _drawImageBox(label, diffImg, diffClusters) { return ( <div className="image-box__image"> <div className="image-box__title">{label}</div> - <Screenshot image={image} diffClusters={diffClusters}/> + <Screenshot image={diffImg} diffClusters={diffClusters}/> </div> ); } } -export default connect(({reporter: {view: {showOnlyDiff}}}) => ({showOnlyDiff}))(StateFail); +export default connect( + ({view: {showOnlyDiff}}) => ({showOnlyDiff}) +)(StateFail); diff --git a/lib/static/components/suites.js b/lib/static/components/suites.js index 222cdbd4b..5ca122165 100644 --- a/lib/static/components/suites.js +++ b/lib/static/components/suites.js @@ -1,50 +1,37 @@ -'use strict'; - import React, {Component} from 'react'; -import {find} from 'lodash'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; -import LazilyRender from '@gemini-testing/react-lazily-render'; import SectionCommon from './section/section-common'; import clientEvents from '../../gui/constants/client-events'; -import {shouldSuiteBeShown} from '../modules/utils'; +import {mkGetVisibleRootSuiteIds} from '../modules/selectors/tree'; class Suites extends Component { static propTypes = { - suiteIds: PropTypes.arrayOf(PropTypes.string), - lazyLoadOffset: PropTypes.number, - errorGroupTests: PropTypes.object + errorGroupBrowserIds: PropTypes.array, + // from store + visibleRootSuiteIds: PropTypes.arrayOf(PropTypes.string), + lazyLoadOffset: PropTypes.number } render() { - const {rootSuites, testNameFilter, strictMatchFilter, filteredBrowsers, lazyLoadOffset, errorGroupTests, viewMode} = this.props; - const visibleRootSuites = rootSuites.filter((suite) => - shouldSuiteBeShown({suite, testNameFilter, strictMatchFilter, filteredBrowsers, errorGroupTests, viewMode}) - ); + const {visibleRootSuiteIds, errorGroupBrowserIds, lazyLoadOffset} = this.props; return ( <div className="sections"> - {visibleRootSuites.map((suite) => { - const suiteId = suite.name; + {visibleRootSuiteIds.map((suiteId) => { const sectionProps = { key: suiteId, - suite: suite, - testNameFilter, - strictMatchFilter, - filteredBrowsers, - errorGroupTests + suiteId: suiteId, + errorGroupBrowserIds, + sectionRoot: true }; if (lazyLoadOffset > 0) { sectionProps.eventToUpdate = clientEvents.VIEW_CHANGED; } - const sectionElem = <SectionCommon {...sectionProps} />; - - return lazyLoadOffset > 0 - ? <LazilyRender eventToUpdate={clientEvents.VIEW_CHANGED} key={suiteId} offset={lazyLoadOffset} content={sectionElem} /> - : sectionElem; + return <SectionCommon {...sectionProps} />; })} </div> ); @@ -52,18 +39,12 @@ class Suites extends Component { } export default connect( - ({reporter: state}) => { - const {testNameFilter, strictMatchFilter, filteredBrowsers, lazyLoadOffset, viewMode} = state.view; - const suiteIds = state.suiteIds[state.view.viewMode]; - const rootSuites = suiteIds.map((id) => find(state.suites, {name: id})); + () => { + const getVisibleRootSuiteIds = mkGetVisibleRootSuiteIds(); - return ({ - testNameFilter, - filteredBrowsers, - strictMatchFilter, - lazyLoadOffset, - viewMode, - rootSuites + return (state, {errorGroupBrowserIds}) => ({ + lazyLoadOffset: state.view.lazyLoadOffset, + visibleRootSuiteIds: getVisibleRootSuiteIds(state, {errorGroupBrowserIds}) }); } )(Suites); diff --git a/lib/static/containers/modal.js b/lib/static/containers/modal.js index bf53ee98b..bea86ddba 100644 --- a/lib/static/containers/modal.js +++ b/lib/static/containers/modal.js @@ -30,6 +30,6 @@ class ModalContainer extends Component { } export default connect( - ({reporter: {modal}}) => ({modal}), + ({modal}) => ({modal}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(ModalContainer); diff --git a/lib/static/modules/action-names.js b/lib/static/modules/action-names.js index 9070fd4a7..92808b76b 100644 --- a/lib/static/modules/action-names.js +++ b/lib/static/modules/action-names.js @@ -1,6 +1,9 @@ 'use strict'; export default { + INIT_GUI_REPORT: 'INIT_GUI_REPORT', + INIT_STATIC_REPORT: 'INIT_STATIC_REPORT', + FIN_STATIC_REPORT: 'FIN_STATIC_REPORT', RUN_ALL_TESTS: 'RUN_ALL_TESTS', RUN_FAILED_TESTS: 'RUN_FAILED_TESTS', RETRY_SUITE: 'RETRY_SUITE', @@ -12,13 +15,11 @@ export default { ACCEPT_SCREENSHOT: 'ACCEPT_SCREENSHOT', ACCEPT_OPENED_SCREENSHOTS: 'ACCEPT_OPENED_SCREENSHOTS', CLOSE_SECTIONS: 'CLOSE_SECTIONS', - TOGGLE_TEST_RESULT: 'TOGGLE_TEST_RESULT', TOGGLE_STATE_RESULT: 'TOGGLE_STATE_RESULT', TOGGLE_LOADING: 'TOGGLE_LOADING', SHOW_MODAL: 'SHOW_MODAL', HIDE_MODAL: 'HIDE_MODAL', CHANGE_TEST_RETRY: 'CHANGE_TEST_RETRY', - VIEW_INITIAL: 'VIEW_INITIAL', VIEW_EXPAND_ALL: 'VIEW_EXPAND_ALL', VIEW_EXPAND_ERRORS: 'VIEW_EXPAND_ERRORS', VIEW_EXPAND_RETRIES: 'VIEW_EXPAND_RETRIES', @@ -35,8 +36,6 @@ export default { VIEW_TOGGLE_LAZY_LOAD_IMAGES: 'VIEW_TOGGLE_LAZY_LOAD_IMAGES', PROCESS_BEGIN: 'PROCESS_BEGIN', PROCESS_END: 'PROCESS_END', - FETCH_DB: 'FETCH_DB', - CLOSE_DB: 'CLOSE_DB', RUN_CUSTOM_GUI_ACTION: 'RUN_CUSTOM_GUI_ACTION', BROWSERS_SELECTED: 'BROWSERS_SELECTED' }; diff --git a/lib/static/modules/actions.js b/lib/static/modules/actions.js index f604f2c29..371ba9421 100644 --- a/lib/static/modules/actions.js +++ b/lib/static/modules/actions.js @@ -1,20 +1,16 @@ 'use strict'; import axios from 'axios'; -import {assign, filter, flatMap, get, cloneDeep, isEmpty, compact, difference, omit} from 'lodash'; +import {isEmpty, difference} from 'lodash'; import {addNotification as notify} from 'reapop'; - +import StaticTestsTreeBuilder from '../../tests-tree-builder/static'; import actionNames from './action-names'; import modalTypes from './modal-types'; -import {QUEUED, UPDATED} from '../../constants/test-statuses'; +import {QUEUED} from '../../constants/test-statuses'; import {VIEW_CHANGED} from '../../constants/client-events'; -import {isSuiteFailed, isAcceptable, getHttpErrorMessage} from './utils'; -import {isSuccessStatus, isFailStatus, isErroredStatus, isIdleStatus} from '../../common-utils'; -import { - getRefImagesInfo, getAllOpenedImagesInfo, getImagesInfoId, filterByBro, rejectRefImagesInfo, - filterStatus, filterImagesInfo, filterByEqualDiffSizes -} from './find-same-diffs-utils'; +import {getHttpErrorMessage} from './utils'; import {fetchDatabases, mergeDatabases} from './sqlite'; +import {getSuitesTableRows} from './database-utils'; import {setFilteredBrowsers} from './query-params'; const createNotification = (id, status, message, props) => @@ -23,30 +19,31 @@ const createNotification = (id, status, message, props) => const createNotificationError = (id, error, props = {dismissAfter: 0}) => createNotification(id, 'error', getHttpErrorMessage(error), props); -export const initial = () => { +export const initGuiReport = () => { return async (dispatch) => { try { const appState = await axios.get('/init'); dispatch({ - type: actionNames.VIEW_INITIAL, + type: actionNames.INIT_GUI_REPORT, payload: appState.data }); const {customGuiError} = appState.data; if (customGuiError) { - dispatch(createNotificationError('initial', {...customGuiError})); + dispatch(createNotificationError('initGuiReport', {...customGuiError})); delete appState.data.customGuiError; } } catch (e) { - dispatch(createNotificationError('initial', e)); + dispatch(createNotificationError('initGuiReport', e)); } }; }; -export const openDbConnection = () => { +export const initStaticReport = () => { return async dispatch => { + const dataFromStaticFile = window.data || {}; let fetchDbDetails = []; let db = null; @@ -60,20 +57,28 @@ export const openDbConnection = () => { db = await mergeDatabases(dataForDbs); } catch (e) { - console.error('Error while fetching databases', e); + dispatch(createNotificationError('initStaticReport', e)); + } + + if (!db || isEmpty(fetchDbDetails)) { + return dispatch({ + type: actionNames.INIT_STATIC_REPORT, + payload: {...dataFromStaticFile, db, fetchDbDetails, tree: {suites: []}, stats: [], skips: [], browsers: []} + }); } + const testsTreeBuilder = StaticTestsTreeBuilder.create(); + const suitesRows = getSuitesTableRows(db); + const {tree, stats, skips, browsers} = testsTreeBuilder.build(suitesRows); + dispatch({ - type: actionNames.FETCH_DB, - payload: { - db, - fetchDbDetails - } + type: actionNames.INIT_STATIC_REPORT, + payload: {...dataFromStaticFile, db, fetchDbDetails, tree, stats, skips, browsers} }); }; }; -export const closeDbConnection = () => ({type: actionNames.CLOSE_DB}); +export const finStaticReport = () => ({type: actionNames.FIN_STATIC_REPORT}); const runTests = ({tests = [], action = {}} = {}) => { return async (dispatch) => { @@ -95,39 +100,25 @@ export const runAllTests = () => { }); }; -export const runFailedTests = (fails, actionName = actionNames.RUN_FAILED_TESTS) => { - fails = filterFailedBrowsers([].concat(fails)); - - return runTests({tests: fails, action: {type: actionName}}); -}; - -export const runSuccessfulTests = (successfulTests, actionName = actionNames.RETRY_TEST) => { - return runTests({tests: successfulTests, action: {type: actionName}}); +export const runFailedTests = (failedTests, actionName = actionNames.RUN_FAILED_TESTS) => { + return runTests({tests: failedTests, action: {type: actionName}}); }; -export const retrySuite = (suite) => { - return runTests({tests: [suite], action: {type: actionNames.RETRY_SUITE}}); +export const retrySuite = (tests) => { + return runTests({tests, action: {type: actionNames.RETRY_SUITE}}); }; -export const retryTest = (suite, browserId) => { - let tests = assign({browserId}, suite); - tests = omit(tests, 'children'); - const browserStatus = tests.browsers.find(({name}) => name === browserId).result.status; - - return isIdleStatus(browserStatus) || isSuccessStatus(browserStatus) - ? runSuccessfulTests(tests, actionNames.RETRY_TEST) - : runFailedTests(tests, actionNames.RETRY_TEST); +export const retryTest = (test) => { + return runTests({tests: [test], action: {type: actionNames.RETRY_TEST}}); }; -export const acceptOpened = (fails, type = actionNames.ACCEPT_OPENED_SCREENSHOTS) => { - fails = filterAcceptableBrowsers([].concat(fails)); - - const formattedFails = flatMap([].concat(fails), formatTests); - +export const acceptOpened = (imageIds, type = actionNames.ACCEPT_OPENED_SCREENSHOTS) => { return async (dispatch) => { dispatch({type: actionNames.PROCESS_BEGIN}); + try { - const {data: updatedData} = await axios.post('/update-reference', compact(formattedFails)); + const {data} = await axios.post('/get-update-reference-data', imageIds); + const {data: updatedData} = await axios.post('/update-reference', data); window.dispatchEvent(new Event(VIEW_CHANGED)); dispatch({type, payload: updatedData}); } catch (e) { @@ -138,18 +129,14 @@ export const acceptOpened = (fails, type = actionNames.ACCEPT_OPENED_SCREENSHOTS }; }; -export const acceptTest = (suite, browserId, stateName) => { - return acceptOpened(assign({browserId, stateName}, suite), actionNames.ACCEPT_SCREENSHOT); +export const acceptTest = (imageId) => { + return acceptOpened([imageId], actionNames.ACCEPT_SCREENSHOT); }; export const suiteBegin = (suite) => ({type: actionNames.SUITE_BEGIN, payload: suite}); export const testBegin = (test) => ({type: actionNames.TEST_BEGIN, payload: test}); export const testResult = (result) => ({type: actionNames.TEST_RESULT, payload: result}); -export const toggleTestResult = (result) => triggerViewChanges({type: actionNames.TOGGLE_TEST_RESULT, payload: result}); -export const toggleStateResult = (result) => triggerViewChanges({ - type: actionNames.TOGGLE_STATE_RESULT, - payload: result -}); +export const toggleStateResult = (result) => triggerViewChanges({type: actionNames.TOGGLE_STATE_RESULT, payload: result}); export const changeTestRetry = (result) => ({type: actionNames.CHANGE_TEST_RETRY, payload: result}); export const toggleLoading = (payload) => ({type: actionNames.TOGGLE_LOADING, payload}); export const closeSections = (payload) => triggerViewChanges({type: actionNames.CLOSE_SECTIONS, payload}); @@ -205,52 +192,37 @@ export function changeViewMode(mode) { } } -export const findSameDiffs = ({suitePath, browser, stateName, fails}) => { +export const findSameDiffs = (selectedImageId, openedImageIds, browserName) => { return async (dispatch) => { dispatch(toggleLoading({active: true, content: 'Find same diffs...'})); - const refImagesInfo = {suitePath, browserId: browser.name, ...getRefImagesInfo({browser, stateName})}; - const refImagesInfoId = getImagesInfoId({suitePath, browserId: browser.name, stateName}); - - const allOpenedImagesInfo = getAllOpenedImagesInfo(fails); - const allOpenedImagesInfoIds = allOpenedImagesInfo.map(getImagesInfoId); - - const comparedImagesInfo = filterImagesInfo( - allOpenedImagesInfo, - [filterByBro(browser.name), rejectRefImagesInfo(refImagesInfoId), filterStatus(isFailStatus)] - ); - - const imagesInfoWithEqualDiffSizes = filterByEqualDiffSizes(comparedImagesInfo, refImagesInfo.diffClusters); - let equalImages = []; + const comparedImageIds = openedImageIds.filter((id) => id.includes(browserName) && id !== selectedImageId); + let equalImagesIds = []; try { - if (isEmpty(imagesInfoWithEqualDiffSizes)) { - const closeImagesInfoIds = difference(allOpenedImagesInfoIds, [refImagesInfoId]); - dispatch(closeSections(closeImagesInfoIds)); - return; - } + if (!isEmpty(comparedImageIds)) { + const {data} = await axios.post('/get-find-equal-diffs-data', [selectedImageId].concat(comparedImageIds)); - equalImages = (await axios.post( - '/find-equal-diffs', - [refImagesInfo].concat(imagesInfoWithEqualDiffSizes)) - ).data; + if (!isEmpty(data)) { + equalImagesIds = (await axios.post('/find-equal-diffs', data)).data; + } + } - const closeImagesInfoIds = difference( - allOpenedImagesInfoIds, - equalImages.map(getImagesInfoId).concat(refImagesInfoId) - ); + const closeImagesIds = difference(openedImageIds, [].concat(selectedImageId, equalImagesIds)); - dispatch(closeSections(closeImagesInfoIds)); + if (!isEmpty(closeImagesIds)) { + dispatch(closeSections(closeImagesIds)); + } } catch (e) { - console.error('Error while trying to find equal diffs of failed tests:', e); + console.error('Error while trying to find equal diffs:', e); } finally { dispatch(toggleLoading({active: false})); dispatch(showModal({ type: modalTypes.FIND_SAME_DIFFS_MODAL, data: { - browserId: browser.name, - equalImages: equalImages.length, - comparedImages: comparedImagesInfo.length + browserId: browserName, + equalImages: equalImagesIds.length, + comparedImages: comparedImageIds.length } })); } @@ -271,99 +243,3 @@ function triggerViewChanges(payload) { return payload; } - -function formatTests(test) { - let resultFromBrowsers = []; - let resultFromChildren = []; - - if (test.children) { - resultFromChildren = flatMap(test.children, formatTests); - } - - if (test.browsers) { - if (test.browserId) { - test.browsers = filter(test.browsers, {name: test.browserId}); - } - - const {suitePath, name} = test; - const prepareImages = (images, filterCond) => { - return filter(images, filterCond) - .filter(isAcceptable) - .map(({actualImg, stateName}) => ({status: UPDATED, actualImg, stateName})); - }; - - resultFromBrowsers = flatMap(test.browsers, (browser) => { - const browserResult = getOpenedBrowserResult(browser); - - let imagesInfo; - if (test.stateName) { - imagesInfo = prepareImages(browserResult.imagesInfo, {stateName: test.stateName}); - } else { - imagesInfo = prepareImages(browserResult.imagesInfo, 'actualImg'); - } - - const {metaInfo, attempt} = browserResult; - - return imagesInfo.length && { - suite: {path: suitePath.slice(0, -1)}, - state: {name}, - browserId: browser.name, - metaInfo, - imagesInfo, - attempt - }; - }); - } - - return [...resultFromBrowsers, ...resultFromChildren]; -} - -// return flat list of nodes with browsers -function filterBrowsers(suites = [], filterFns, filterBrowserResult) { - const modifySuite = (suite) => { - let resultFromBrowsers = []; - let resultFromChildren = []; - - if (suite.children) { - resultFromChildren = flatMap(suite.children, modifySuite); - } - - if (suite.browsers) { - suite.browsers = filter(suite.browsers, (bro) => { - if (suite.browserId && suite.browserId !== bro.name) { - return false; - } - - const broResult = filterBrowserResult ? filterBrowserResult(bro) : bro.result; - - return [].concat(filterFns).every((fn) => fn(broResult)); - }); - resultFromBrowsers.push(omit(suite, 'children')); - } - - return [...resultFromBrowsers, ...resultFromChildren]; - }; - - return flatMap(cloneDeep(suites), modifySuite); -} - -function filterFailedBrowsers(suites = []) { - return filterBrowsers(suites, isSuiteFailed); -} - -function filterAcceptableBrowsers(suites = []) { - return filterBrowsers(suites, [isAcceptable, filterOpenedStateResult], getOpenedBrowserResult); -} - -function getOpenedBrowserResult(bro) { - const {result, retries, state} = bro; - return get(state, 'opened') ? retries.concat(result)[state.retryIndex] : {}; -} - -function filterOpenedStateResult(broResult) { - broResult.imagesInfo = broResult.imagesInfo.filter(({opened, status}) => { - return opened && (isFailStatus(status) || isErroredStatus(status)); - }); - - return broResult.imagesInfo.length; -} diff --git a/lib/static/modules/default-state.js b/lib/static/modules/default-state.js index db989b5d7..b286ff4ed 100644 --- a/lib/static/modules/default-state.js +++ b/lib/static/modules/default-state.js @@ -11,10 +11,27 @@ export default Object.assign(defaults, { skips: [], browsers: [], groupedErrors: [], - suites: {}, - suiteIds: { - all: [], - failed: [] + tree: { + suites: { + byId: {}, + allIds: [], + allRootIds: [], + failedRootIds: [] + }, + browsers: { + byId: {}, + stateById: {}, + allIds: [] + }, + results: { + byId: {}, + allIds: [] + }, + images: { + byId: {}, + stateById: {}, + allIds: [] + } }, closeIds: [], apiValues: { @@ -24,13 +41,16 @@ export default Object.assign(defaults, { loading: {}, modal: {}, stats: { - total: 0, - updated: 0, - passed: 0, - failed: 0, - skipped: 0, - retries: 0, - warned: 0 + all: { + total: 0, + updated: 0, + passed: 0, + failed: 0, + skipped: 0, + retries: 0, + warned: 0 + }, + perBrowser: {} }, view: { viewMode: viewModes.ALL, @@ -45,5 +65,5 @@ export default Object.assign(defaults, { groupByError: false }, db: undefined, - fetchDbDetails: undefined + fetchDbDetails: [] }); diff --git a/lib/static/modules/find-same-diffs-utils.js b/lib/static/modules/find-same-diffs-utils.js deleted file mode 100644 index f80974e6a..000000000 --- a/lib/static/modules/find-same-diffs-utils.js +++ /dev/null @@ -1,128 +0,0 @@ -'use strict'; - -import {get, find, findIndex, filter, flatMap, isEqual} from 'lodash'; - -const getDiffClusterSizes = (diffCluster) => { - return { - width: diffCluster.right - diffCluster.left + 1, - height: diffCluster.bottom - diffCluster.top + 1 - }; -}; - -const compareDiffSizes = (diffSizes1, diffSizes2) => { - if (diffSizes1.length !== diffSizes2.length) { - return false; - } - - return diffSizes1.every((diffSize) => { - const foundIndex = findIndex(diffSizes2, diffSize); - - if (foundIndex < 0) { - return false; - } - - diffSizes2 = diffSizes2.filter((v, ind) => ind !== foundIndex); - - return true; - }); -}; - -const reorderClustersByEqualSize = (diffClusters1, diffSizes1, diffSizes2) => { - return diffClusters1.reduce((acc, cluster, i) => { - if (diffSizes1[i] !== diffSizes2[i]) { - const foundIndex = findIndex(diffSizes2, diffSizes1[i]); - diffSizes2 = diffSizes2.filter((v, ind) => ind !== foundIndex); - acc[foundIndex] = cluster; - } else { - acc[i] = cluster; - } - - return acc; - }, []); -}; - -export const filterByEqualDiffSizes = (imagesInfo, refDiffClusters) => { - const refDiffSizes = refDiffClusters.map(getDiffClusterSizes); - - return filter(imagesInfo, (imageInfo) => { - const imageDiffSizes = imageInfo.diffClusters.map(getDiffClusterSizes); - const equal = compareDiffSizes(imageDiffSizes, refDiffSizes); - - if (!equal) { - return false; - } - - if (!isEqual(imageDiffSizes, refDiffSizes)) { - imageInfo.diffClusters = reorderClustersByEqualSize(imageInfo.diffClusters, imageDiffSizes, refDiffSizes); - } - - return true; - }); -}; - -export const getImagesInfoId = ({suitePath, browserId, stateName = ''}) => { - return suitePath.concat(browserId, stateName).join(' '); -}; - -export const getRefImagesInfo = ({browser, stateName}) => { - const {retryIndex} = browser.state; - const browserResult = browser.retries.concat(browser.result)[retryIndex]; - - return stateName ? find(browserResult.imagesInfo, {stateName}) : browserResult.imagesInfo[0]; -}; - -export const getAllOpenedImagesInfo = (fails = []) => { - const findImagesInfoForBrowser = (node) => { - const {retryIndex} = node.state; - const broResult = node.retries.concat(node.result)[retryIndex]; - - return broResult.imagesInfo.map((imageInfo) => { - if (!imageInfo.opened) { - return null; - } - - return {suitePath: node.suitePath, browserId: node.name, ...imageInfo}; - }).filter((v) => v); - }; - - const findImagesInfoRecursive = (node) => { - let resultFromBrowsers = []; - let resultFromChildren = []; - - if (node.children) { - resultFromChildren = flatMap(node.children, findImagesInfoRecursive); - } - - if (node.browsers) { - resultFromBrowsers = flatMap(node.browsers, (bro) => { - if (!get(bro, 'state.opened')) { - return []; - } - - return findImagesInfoForBrowser({suitePath: node.suitePath, ...bro}); - }); - } - - return [...resultFromBrowsers, ...resultFromChildren]; - }; - - return flatMap(fails, findImagesInfoRecursive); -}; - -export const filterByBro = (browserId) => { - return (imageInfo) => imageInfo.browserId === browserId; -}; - -export const rejectRefImagesInfo = (imagesInfoId) => { - return (imageInfo) => getImagesInfoId(imageInfo) !== imagesInfoId; -}; - -export const filterStatus = (checkStatusFn) => { - return (imageInfo) => checkStatusFn(imageInfo.status); -}; - -export const filterImagesInfo = (imagesInfo, filterFns) => { - return imagesInfo.filter((imageInfo) => { - return [].concat(filterFns).every((fn) => fn(imageInfo)); - }); -}; diff --git a/lib/static/modules/group-errors.js b/lib/static/modules/group-errors.js index f8a04a6da..fd5484aad 100644 --- a/lib/static/modules/group-errors.js +++ b/lib/static/modules/group-errors.js @@ -1,14 +1,12 @@ -'use strict'; - -import {get, filter} from 'lodash'; -import {isSuiteFailed, isTestNameMatchFilters, shouldShowBrowser} from './utils'; -import {isFailStatus, isErroredStatus} from '../../common-utils'; +import {get} from 'lodash'; +import {isTestNameMatchFilters} from './utils'; +import {shouldShowBrowser, getFailedSuiteResults} from './selectors/tree'; import viewModes from '../../constants/view-modes'; const imageComparisonErrorMessage = 'image comparison failed'; /** - * @param {object} suites + * @param {object} tree * @param {string} viewMode * @param {array} errorPatterns * @param {array} filteredBrowsers @@ -16,118 +14,65 @@ const imageComparisonErrorMessage = 'image comparison failed'; * @param {boolean} [strictMatchFilter] * @return {array} */ -function groupErrors({suites = {}, viewMode = viewModes.ALL, errorPatterns = [], filteredBrowsers = [], testNameFilter = '', strictMatchFilter = false}) { +function groupErrors({tree = {}, viewMode = viewModes.ALL, errorPatterns = [], filteredBrowsers = [], testNameFilter = '', strictMatchFilter = false}) { const showOnlyFailed = viewMode === viewModes.FAILED; - const filteredSuites = showOnlyFailed ? filter(suites, isSuiteFailed) : suites; - const testWithErrors = extractErrors(filteredSuites, showOnlyFailed); - const errorGroupsList = getErrorGroupList(testWithErrors, errorPatterns, filteredBrowsers, testNameFilter, strictMatchFilter); - - errorGroupsList.sort((a, b) => { - const result = b.count - a.count; - if (result === 0) { - return a.name.localeCompare(b.name); - } - return result; - }); - - return errorGroupsList; -} + const failedResults = showOnlyFailed ? getFailedSuiteResults(tree) : Object.values(tree.results.byId); + const errorGroups = {}; -function extractErrors(rootSuites, showOnlyFailed) { - 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) { - const {status} = browser.result; - if (showOnlyFailed && !isFailStatus(status) && !isErroredStatus(status)) { - continue; - } - const retries = [...browser.retries, browser.result]; - const errors = extractErrorsFromRetries(retries); - - if (errors.length) { - browsersWithError.push({ - browser, - errors - }); - } - } - } + for (const result of failedResults) { + const browser = tree.browsers.byId[result.parentId]; + const images = result.imageIds.map((imageId) => tree.images.byId[imageId]); + const testName = browser.parentId; - if (Object.keys(browsersWithError).length) { - testWithErrors[testName] = browsersWithError; - } - - if (suite.children) { - extract(suite.children); - } + if (!isTestNameMatchFilters(testName, testNameFilter, strictMatchFilter)) { + continue; } - }; - extract(rootSuites); + if (!shouldShowBrowser(browser, filteredBrowsers)) { + continue; + } - return testWithErrors; -} + const errors = extractErrors(result, images); -function extractErrorsFromRetries(retries) { - const errorsInRetry = new Set(); + for (const errorText of errors) { + const patternInfo = matchGroup(errorText, errorPatterns); + const {pattern, name} = patternInfo; - for (const retry of retries) { - for (const {error, diffImg} of [...retry.imagesInfo, retry]) { - if (get(error, 'message')) { - errorsInRetry.add(error.message); + if (!errorGroups.hasOwnProperty(name)) { + errorGroups[name] = { + pattern, + name, + browserIds: [], + count: 0 + }; } - if (diffImg) { - errorsInRetry.add(imageComparisonErrorMessage); + + const group = errorGroups[name]; + + if (!group.browserIds.includes(browser.id)) { + group.browserIds.push(browser.id); + group.count++; } } } - return [...errorsInRetry]; + + return Object.values(errorGroups); } -function getErrorGroupList(testWithErrors, errorPatterns, filteredBrowsers, testNameFilter, strictMatchFilter) { - const errorGroups = {}; +function extractErrors(result, images) { + const errors = new Set(); - for (const [testName, browsers] of Object.entries(testWithErrors)) { - if (!isTestNameMatchFilters(testName, testNameFilter, strictMatchFilter)) { - continue; + for (const {error, diffImg} of [...images, result]) { + if (get(error, 'message')) { + errors.add(error.message); } - for (const {browser, errors} of browsers) { - if (!shouldShowBrowser(browser, filteredBrowsers)) { - continue; - } - - for (const errorText of errors) { - const patternInfo = matchGroup(errorText, errorPatterns); - 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(browser.name)) { - group.tests[testName].push(browser.name); - group.count++; - } - } + if (diffImg) { + errors.add(imageComparisonErrorMessage); } } - return Object.values(errorGroups); + return [...errors]; } function matchGroup(errorText, errorPatterns) { diff --git a/lib/static/modules/reducers/helpers/local-storage-wrapper.js b/lib/static/modules/local-storage-wrapper.js similarity index 66% rename from lib/static/modules/reducers/helpers/local-storage-wrapper.js rename to lib/static/modules/local-storage-wrapper.js index 9040b2d27..7fe9c8e06 100644 --- a/lib/static/modules/reducers/helpers/local-storage-wrapper.js +++ b/lib/static/modules/local-storage-wrapper.js @@ -1,9 +1,6 @@ 'use strict'; const _prefix = 'html-reporter'; -const _deprecatedKeysCollection = [ - {deprecatedKey: '_gemini-replace-host', newKey: _getStorageKey('replace-host')} -]; /** * Format Storage key @@ -15,19 +12,6 @@ function _getStorageKey(key) { return `${_prefix}:${key}`; } -/** - * Helping to migrate on new storage keys - * @private - */ -export function updateDeprecatedKeys() { - _deprecatedKeysCollection.forEach(({deprecatedKey, newKey}) => { - if (window.localStorage.hasOwnProperty(deprecatedKey)) { - window.localStorage.setItem(newKey, JSON.stringify(window.localStorage.getItem(deprecatedKey))); - window.localStorage.removeItem(deprecatedKey); - } - }); -} - /** * Wrap localStorage#setItem method * Support value serialization diff --git a/lib/static/modules/middlewares/local-storage.js b/lib/static/modules/middlewares/local-storage.js new file mode 100644 index 000000000..6e84ede0d --- /dev/null +++ b/lib/static/modules/middlewares/local-storage.js @@ -0,0 +1,31 @@ +import * as localStorageWrapper from '../local-storage-wrapper'; +import actionNames from '../action-names'; + +export default store => next => action => { + const result = next(action); + + if (shouldUpdateLocalStorage(action.type)) { + const {view} = store.getState(); + // do not save text inputs: + // for example, a user opens a new report and sees no tests in it + // as the filter is applied from the previous opening of another report + localStorageWrapper.setItem('view', { + expand: view.expand, + viewMode: view.viewMode, + showSkipped: view.showSkipped, + showOnlyDiff: view.showOnlyDiff, + scaleImages: view.scaleImages, + // TODO: Uncomment when issues with rendering speed will fixed + // lazyLoadOffset: view.lazyLoadOffset, + groupByError: view.groupByError, + strictMatchFilter: view.strictMatchFilter + }); + } + + return result; +}; + +function shouldUpdateLocalStorage(actionType) { + return /^VIEW_/.test(actionType) + || [actionNames.INIT_GUI_REPORT, actionNames.INIT_STATIC_REPORT].includes(actionType); +} diff --git a/lib/static/modules/middlewares/metrika.js b/lib/static/modules/middlewares/metrika.js index fac58b098..0b2cf403a 100644 --- a/lib/static/modules/middlewares/metrika.js +++ b/lib/static/modules/middlewares/metrika.js @@ -5,7 +5,7 @@ let metrika; export default metrikaClass => store => next => action => { switch (action.type) { - case actionNames.VIEW_INITIAL: { + case actionNames.INIT_GUI_REPORT: { const result = next(action); const config = get(store.getState(), 'reporter.config.yandexMetrika', {}); diff --git a/lib/static/modules/reducers/api-values.js b/lib/static/modules/reducers/api-values.js new file mode 100644 index 000000000..a91ad223e --- /dev/null +++ b/lib/static/modules/reducers/api-values.js @@ -0,0 +1,25 @@ +import actionNames from '../action-names'; + +export default (state, action) => { + switch (action.type) { + case actionNames.INIT_GUI_REPORT: + case actionNames.INIT_STATIC_REPORT: { + const {apiValues} = action.payload; + + return applyChanges(state, apiValues); + } + + default: + return state; + } +}; + +function applyChanges(state, apiValues) { + return { + ...state, + apiValues: { + ...state.apiValues, + ...apiValues + } + }; +} diff --git a/lib/static/modules/reducers/auto-run.js b/lib/static/modules/reducers/auto-run.js new file mode 100644 index 000000000..3b5bb1d07 --- /dev/null +++ b/lib/static/modules/reducers/auto-run.js @@ -0,0 +1,14 @@ +import actionNames from '../action-names'; + +export default (state, action) => { + switch (action.type) { + case actionNames.INIT_GUI_REPORT: { + const {autoRun = false} = action.payload; + + return {...state, autoRun}; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/browsers.js b/lib/static/modules/reducers/browsers.js new file mode 100644 index 000000000..65d8c651e --- /dev/null +++ b/lib/static/modules/reducers/browsers.js @@ -0,0 +1,42 @@ +import _ from 'lodash'; +import actionNames from '../action-names'; +import {versions as BrowserVersions} from '../../../constants/browser'; + +export default (state, action) => { + switch (action.type) { + case actionNames.INIT_GUI_REPORT: { + const browsers = extractBrowsers(state.tree.results); + + return {...state, browsers}; + } + + case actionNames.INIT_STATIC_REPORT: { + const {browsers} = action.payload; + + return {...state, browsers}; + } + + default: + return state; + } +}; + +function extractBrowsers(results) { + const getVersion = (result) => ({ + id: result.name, + versions: _.get(result, 'metaInfo.browserVersion', BrowserVersions.UNKNOWN) + }); + const getUniqVersions = (set) => _(set) + .map('versions') + .compact() + .uniq() + .value(); + + return _(results.allIds) + .map((resultId) => getVersion(results.byId[resultId])) + .flattenDeep() + .groupBy('id') + .mapValues(getUniqVersions) + .map((versions, id) => ({id, versions})) + .value(); +} diff --git a/lib/static/modules/reducers/close-ids.js b/lib/static/modules/reducers/close-ids.js new file mode 100644 index 000000000..bf6b02eda --- /dev/null +++ b/lib/static/modules/reducers/close-ids.js @@ -0,0 +1,12 @@ +import actionNames from '../action-names'; + +export default (state, action) => { + switch (action.type) { + case actionNames.CLOSE_SECTIONS: { + return {...state, closeIds: action.payload}; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/config.js b/lib/static/modules/reducers/config.js new file mode 100644 index 000000000..91a5b7477 --- /dev/null +++ b/lib/static/modules/reducers/config.js @@ -0,0 +1,48 @@ +import {cloneDeep} from 'lodash'; +import {CONTROL_TYPE_RADIOBUTTON} from '../../../gui/constants/custom-gui-control-types'; +import actionNames from '../action-names'; + +export default (state, action) => { + switch (action.type) { + case actionNames.INIT_GUI_REPORT: + case actionNames.INIT_STATIC_REPORT: { + const {config} = action.payload; + + config.errorPatterns = formatErrorPatterns(config.errorPatterns); + + return applyChanges(state, config); + } + + case actionNames.RUN_CUSTOM_GUI_ACTION: { + const {sectionName, groupIndex, controlIndex} = action.payload; + + const customGui = cloneDeep(state.config.customGui); + const {type, controls} = customGui[sectionName][groupIndex]; + + if (type === CONTROL_TYPE_RADIOBUTTON) { + controls.forEach((control, i) => control.active = (controlIndex === i)); + + return applyChanges(state, {customGui}); + } + + return state; + } + + default: + return state; + } +}; + +function formatErrorPatterns(errorPatterns) { + return errorPatterns.map((patternInfo) => ({...patternInfo, regexp: new RegExp(patternInfo.pattern)})); +} + +function applyChanges(state, config) { + return { + ...state, + config: { + ...state.config, + ...config + } + }; +} diff --git a/lib/static/modules/reducers/date.js b/lib/static/modules/reducers/date.js new file mode 100644 index 000000000..65791ccfa --- /dev/null +++ b/lib/static/modules/reducers/date.js @@ -0,0 +1,16 @@ +import actionNames from '../action-names'; +import {dateToLocaleString} from '../utils'; + +export default (state, action) => { + switch (action.type) { + case actionNames.INIT_GUI_REPORT: + case actionNames.INIT_STATIC_REPORT: { + const {date} = action.payload; + + return {...state, date: dateToLocaleString(date)}; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/db.js b/lib/static/modules/reducers/db.js new file mode 100644 index 000000000..553d8891e --- /dev/null +++ b/lib/static/modules/reducers/db.js @@ -0,0 +1,20 @@ +import {closeDatabase} from '../database-utils'; +import actionNames from '../action-names'; + +export default (state, action) => { + switch (action.type) { + case actionNames.INIT_STATIC_REPORT: { + const {db} = action.payload; + return {...state, db}; + } + + case actionNames.FIN_STATIC_REPORT: { + closeDatabase(state.db); + + return state; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/fetch-db-details.js b/lib/static/modules/reducers/fetch-db-details.js new file mode 100644 index 000000000..00167f613 --- /dev/null +++ b/lib/static/modules/reducers/fetch-db-details.js @@ -0,0 +1,13 @@ +import actionNames from '../action-names'; + +export default (state, action) => { + switch (action.type) { + case actionNames.INIT_STATIC_REPORT: { + const {fetchDbDetails} = action.payload; + return {...state, fetchDbDetails}; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/grouped-errors.js b/lib/static/modules/reducers/grouped-errors.js new file mode 100644 index 000000000..516c12379 --- /dev/null +++ b/lib/static/modules/reducers/grouped-errors.js @@ -0,0 +1,43 @@ +import {groupErrors} from '../group-errors'; +import {getViewQuery} from '../custom-queries'; +import actionNames from '../action-names'; + +export default (state, action) => { + if (!state.view.groupByError) { + return state; + } + + switch (action.type) { + case actionNames.INIT_GUI_REPORT: + case actionNames.INIT_STATIC_REPORT: { + const viewQuery = getViewQuery(window.location.search); + const {tree, config: {errorPatterns}, view: {viewMode}} = state; + const groupedErrors = groupErrors({tree, viewMode, errorPatterns, ...viewQuery}); + + return {...state, groupedErrors}; + } + + case actionNames.TESTS_END: + case actionNames.BROWSERS_SELECTED: + case actionNames.ACCEPT_SCREENSHOT: + case actionNames.ACCEPT_OPENED_SCREENSHOTS: + case actionNames.VIEW_UPDATE_FILTER_BY_NAME: + case actionNames.VIEW_SET_STRICT_MATCH_FILTER: + case actionNames.VIEW_TOGGLE_GROUP_BY_ERROR: + case actionNames.VIEW_SHOW_ALL: + case actionNames.VIEW_SHOW_FAILED: { + const { + tree, + config: {errorPatterns}, + view: {viewMode, filteredBrowsers, testNameFilter, strictMatchFilter} + } = state; + + const groupedErrors = groupErrors({tree, viewMode, errorPatterns, filteredBrowsers, testNameFilter, strictMatchFilter}); + + return {...state, groupedErrors}; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/gui.js b/lib/static/modules/reducers/gui.js new file mode 100644 index 000000000..e3fd36747 --- /dev/null +++ b/lib/static/modules/reducers/gui.js @@ -0,0 +1,16 @@ +import actionNames from '../action-names'; + +export default (state, action) => { + switch (action.type) { + case actionNames.INIT_GUI_REPORT: { + return {...state, gui: true}; + } + + case actionNames.INIT_STATIC_REPORT: { + return {...state, gui: false}; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/index.js b/lib/static/modules/reducers/index.js index c4e9cf5ce..58f5297ec 100644 --- a/lib/static/modules/reducers/index.js +++ b/lib/static/modules/reducers/index.js @@ -1,16 +1,49 @@ -'use strict'; +import {isEmpty} from 'lodash'; +import reduceReducers from 'reduce-reducers'; +import defaultState from '../default-state'; -import {combineReducers} from 'redux'; -import {POSITIONS, reducer as notificationsReducer} from 'reapop'; -import reporter from './reporter'; +import notifications from './notifications'; +import running from './running'; +import processing from './processing'; +import loading from './loading'; +import gui from './gui'; +import date from './date'; +import autoRun from './auto-run'; +import apiValues from './api-values'; +import modal from './modal'; +import closeIds from './close-ids'; +import db from './db'; +import fetchDbDetails from './fetch-db-details'; +import tree from './tree'; +import browsers from './browsers'; +import config from './config'; +import stats from './stats'; +import skips from './skips'; +import view from './view'; +import groupedErrors from './grouped-errors'; -export default combineReducers({ - notifications: notificationsReducer({ - position: POSITIONS.topCenter, - dismissAfter: 5000, - dismissible: true, - closeButton: true, - allowHTML: true - }), - reporter -}); +// The order of specifying reducers is important. +// At the top specify reducers that does not depend on other state fields. +// At the bottom specify reducers that depend on state from other reducers listed above. +export default reduceReducers( + (state) => isEmpty(state) ? defaultState : state, + notifications, + running, + processing, + loading, + gui, + date, + autoRun, + apiValues, + modal, + closeIds, + db, + fetchDbDetails, + tree, + browsers, + config, + stats, + skips, + view, + groupedErrors +); diff --git a/lib/static/modules/reducers/loading.js b/lib/static/modules/reducers/loading.js new file mode 100644 index 000000000..25c1e1cdf --- /dev/null +++ b/lib/static/modules/reducers/loading.js @@ -0,0 +1,12 @@ +import actionNames from '../action-names'; + +export default (state = {}, action) => { + switch (action.type) { + case actionNames.TOGGLE_LOADING: { + return {...state, loading: action.payload}; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/modal.js b/lib/static/modules/reducers/modal.js new file mode 100644 index 000000000..a1d0c76b8 --- /dev/null +++ b/lib/static/modules/reducers/modal.js @@ -0,0 +1,16 @@ +import actionNames from '../action-names'; + +export default (state = {}, action) => { + switch (action.type) { + case actionNames.SHOW_MODAL: { + return {...state, modal: action.payload}; + } + + case actionNames.HIDE_MODAL: { + return {...state, modal: {}}; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/notifications.js b/lib/static/modules/reducers/notifications.js new file mode 100644 index 000000000..b9184aa4a --- /dev/null +++ b/lib/static/modules/reducers/notifications.js @@ -0,0 +1,16 @@ +import {POSITIONS, reducer} from 'reapop'; + +const notificationsReducer = reducer({ + position: POSITIONS.topCenter, + dismissAfter: 5000, + dismissible: true, + closeButton: true, + allowHTML: true +}); + +export default (state, action) => { + return { + ...state, + notifications: notificationsReducer(state.notifications, action) + }; +}; diff --git a/lib/static/modules/reducers/processing.js b/lib/static/modules/reducers/processing.js new file mode 100644 index 000000000..e5f13a544 --- /dev/null +++ b/lib/static/modules/reducers/processing.js @@ -0,0 +1,21 @@ +import actionNames from '../action-names'; + +export default (state = {}, action) => { + switch (action.type) { + case actionNames.RUN_ALL_TESTS: + case actionNames.RUN_FAILED_TESTS: + case actionNames.RETRY_SUITE: + case actionNames.RETRY_TEST: + case actionNames.PROCESS_BEGIN: { + return {...state, processing: true}; + } + + case actionNames.TESTS_END: + case actionNames.PROCESS_END: { + return {...state, processing: false}; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/reporter.js b/lib/static/modules/reducers/reporter.js deleted file mode 100644 index 6db9a1b7d..000000000 --- a/lib/static/modules/reducers/reporter.js +++ /dev/null @@ -1,560 +0,0 @@ -'use strict'; - -import url from 'url'; -import _ from 'lodash'; -import {assign, clone, cloneDeep, filter, find, last, map, merge, reduce, isEmpty} from 'lodash'; -import StaticTestsTreeBuilder from '../../../tests-tree-builder/static'; -import actionNames from '../action-names'; -import defaultState from '../default-state'; -import { - dateToLocaleString, - findNode, - isSuiteFailed, - setStatusForBranch, - setStatusToAll -} from '../utils'; - -import { - getSuitesTableRows, - closeDatabase -} from '../database-utils'; -import {groupErrors} from '../group-errors'; -import * as localStorageWrapper from './helpers/local-storage-wrapper'; -import {getViewQuery} from '../custom-queries'; -import viewModes from '../../../constants/view-modes'; -import {versions as BrowserVersions} from '../../../constants/browser'; -import {CONTROL_TYPE_RADIOBUTTON} from '../../../gui/constants/custom-gui-control-types'; - -const compiledData = window.data || defaultState; - -function getInitialState(data) { - const { - skips, suites, config, total, updated, passed, - failed, skipped, warned, retries, perBrowser, apiValues, gui = false, autoRun, date - } = data; - - config.errorPatterns = config.errorPatterns.map((patternInfo) => ({...patternInfo, regexp: new RegExp(patternInfo.pattern)})); - - const {errorPatterns, scaleImages, lazyLoadOffset, defaultView: viewMode} = config; - const viewQuery = getViewQuery(window.location.search); - - let formattedSuites = {}; - - if (suites) { - formattedSuites = formatSuitesData(suites); - } - - const browsers = extractBrowsers(formattedSuites.suites); - - if (isEmpty(viewQuery.filteredBrowsers)) { - viewQuery.filteredBrowsers = browsers; - } - - const groupedErrors = groupErrors( - assign({ - suites: formattedSuites.suites, - viewMode, - errorPatterns - }, viewQuery) - ); - - localStorageWrapper.updateDeprecatedKeys(); - const view = localStorageWrapper.getItem('view', {}); - const host = compiledData.config.baseHost; - - return merge({}, defaultState, { - gui, - autoRun, - skips, - groupedErrors, - config, - apiValues, - date: dateToLocaleString(date), - stats: { - all: {total, updated, passed, failed, skipped, retries, warned}, - perBrowser - }, - view: merge({ - viewMode, - scaleImages, - lazyLoadOffset - }, host, view, viewQuery), - browsers - }, formattedSuites); -} - -export default withBrowserStorage(reducer); -function reducer(state = getInitialState(compiledData), action) { - switch (action.type) { - case actionNames.VIEW_INITIAL: { - return getInitialState({...compiledData, ...action.payload}); - } - case actionNames.RUN_ALL_TESTS: { - const suites = clone(state.suites); - Object.values(suites).forEach(suite => setStatusToAll(suite, action.payload.status)); - - // TODO: rewrite store on run all tests - return merge({}, state, {running: true, processing: true, suites, view: {groupByError: false}}); - } - case actionNames.RUN_FAILED_TESTS: - case actionNames.RETRY_SUITE: - case actionNames.RETRY_TEST: { - return { - ...state, - running: true, - processing: true, - view: { - ...state.view, - groupByError: false - } - }; - } - case actionNames.SUITE_BEGIN: { - const suites = clone(state.suites); - const {suitePath, status} = action.payload; - const test = findNode(suites, suitePath); - if (test) { - test.status = status; - forceUpdateSuiteData(suites, test); - } - - return assign({}, state, {suites}); - } - case actionNames.TEST_BEGIN: { - const suites = clone(state.suites); - const {suitePath, status, browserId} = action.payload; - const test = findNode(suites, suitePath); - if (test) { - test.status = status; - test.browsers.forEach((b) => { - if (b.name === browserId) { - b.result.status = status; - } - }); - forceUpdateSuiteData(suites, test); - } - - return assign({}, state, {suites}); - } - case actionNames.TESTS_END: { - return assign(clone(state), {running: false, processing: false}); - } - case actionNames.TEST_RESULT: { - return addTestResult(state, action); - } - case actionNames.PROCESS_BEGIN: { - return assign(clone(state), {processing: true}); - } - case actionNames.PROCESS_END: { - return assign(clone(state), {processing: false}); - } - case actionNames.ACCEPT_SCREENSHOT: { - return addTestResult(state, action); - } - case actionNames.ACCEPT_OPENED_SCREENSHOTS: { - return addTestResult(state, action); - } - case actionNames.VIEW_EXPAND_ALL: { - return _mutateStateView(state, {expand: 'all'}); - } - case actionNames.VIEW_EXPAND_ERRORS: { - return _mutateStateView(state, {expand: 'errors'}); - } - case actionNames.VIEW_EXPAND_RETRIES: { - return _mutateStateView(state, {expand: 'retries'}); - } - case actionNames.VIEW_COLLAPSE_ALL: { - return _mutateStateView(state, {expand: 'none'}); - } - case actionNames.VIEW_SHOW_ALL: { - return _mutateViewMode(state, viewModes.ALL); - } - case actionNames.VIEW_SHOW_FAILED: { - return _mutateViewMode(state, viewModes.FAILED); - } - case actionNames.VIEW_TOGGLE_SKIPPED: { - return _mutateStateView(state, {showSkipped: !state.view.showSkipped}); - } - case actionNames.VIEW_TOGGLE_ONLY_DIFF: { - return _mutateStateView(state, {showOnlyDiff: !state.view.showOnlyDiff}); - } - case actionNames.VIEW_TOGGLE_SCALE_IMAGES: { - return _mutateStateView(state, {scaleImages: !state.view.scaleImages}); - } - case actionNames.VIEW_TOGGLE_LAZY_LOAD_IMAGES: { - return _mutateStateView(state, {lazyLoadOffset: state.view.lazyLoadOffset ? 0 : state.config.lazyLoadOffset}); - } - case actionNames.VIEW_UPDATE_BASE_HOST: { - const baseHost = action.host; - const parsedHost = _parseHost(baseHost); - - return _mutateStateView(state, {baseHost, parsedHost}); - } - case actionNames.VIEW_UPDATE_FILTER_BY_NAME: { - const {testNameFilter} = action; - const { - suites, - config: {errorPatterns}, - view: {viewMode, filteredBrowsers, strictMatchFilter} - } = state; - - const groupedErrors = groupErrors({suites, viewMode, errorPatterns, filteredBrowsers, testNameFilter, strictMatchFilter}); - - return { - ...state, - groupedErrors, - view: { - ...state.view, - testNameFilter - } - }; - } - case actionNames.VIEW_SET_STRICT_MATCH_FILTER: { - const {strictMatchFilter} = action; - const { - suites, - config: {errorPatterns}, - view: {viewMode, filteredBrowsers, testNameFilter} - } = state; - - const groupedErrors = groupErrors({suites, viewMode, errorPatterns, filteredBrowsers, testNameFilter, strictMatchFilter}); - - return { - ...state, - groupedErrors, - view: { - ...state.view, - strictMatchFilter - } - }; - } - 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}); - } - case actionNames.TOGGLE_STATE_RESULT: { - return updateStateResult(state, action); - } - case actionNames.TOGGLE_LOADING: { - const loading = action.payload; - return assign(clone(state), {loading}); - } - case actionNames.SHOW_MODAL: { - const modal = action.payload; - return assign(clone(state), {modal}); - } - case actionNames.HIDE_MODAL: { - return assign(clone(state), {modal: {}}); - } - case actionNames.CHANGE_TEST_RETRY: { - const {retryIndex} = action.payload; - return updateTestState(state, action, {retryIndex}); - } - case actionNames.FETCH_DB: { - return createTestResultsFromDb(state, action); - } - case actionNames.CLOSE_DB: { - return closeDb(state); - } - case actionNames.RUN_CUSTOM_GUI_ACTION: { - const {sectionName, groupIndex, controlIndex} = action.payload; - - const customGui = cloneDeep(state.config.customGui); - const {type, controls} = customGui[sectionName][groupIndex]; - - if (type === CONTROL_TYPE_RADIOBUTTON) { - controls.forEach((control, i) => control.active = (controlIndex === i)); - - return { - ...state, - config: { - ...state.config, - customGui - } - }; - } - - return state; - } - case actionNames.BROWSERS_SELECTED: { - const {browsers} = action.payload; - const groupedErrors = groupErrors({ - suites: state.suites, - viewMode: state.view.viewMode, - errorPatterns: state.config.errorPatterns, - testNameFilter: state.view.testNameFilter, - strictMatchFilter: state.view.strictMatchFilter, - filteredBrowsers: browsers - }); - const view = clone(state.view); - - view.filteredBrowsers = browsers; - - return assign(clone(state), {view, groupedErrors}); - } - default: - return state; - } -} - -function createTestResultsFromDb(state, action) { - const {db, fetchDbDetails} = action.payload; - const testsTreeBuilder = StaticTestsTreeBuilder.create(); - - if (fetchDbDetails.length === 0) { - return { - ...state, - fetchDbDetails - }; - } - - if (!db) { - console.error('There was an error creating the result database.'); - return { - ...state, - fetchDbDetails - }; - } - - const suitesRows = getSuitesTableRows(db); - const {tree: {suites}, stats, skips, browsers} = testsTreeBuilder.build(suitesRows); - const {perBrowser, ...restStats} = stats; - const suiteIds = { - all: getSuiteIds(suites).sort(), - failed: getFailedSuiteIds(suites).sort() - }; - const viewQuery = getViewQuery(window.location.search); - const groupedErrors = groupErrors( - assign({ - suites, - viewMode: state.view.viewMode, - errorPatterns: state.config.errorPatterns - }, viewQuery) - ); - - return { - ...state, - db, - suites, - suiteIds, - fetchDbDetails, - stats: { - all: restStats, - perBrowser - }, - skips, - browsers, - view: merge(state.view, { - browsers, - filteredBrowsers: _.isEmpty(viewQuery.filteredBrowsers) - ? browsers - : viewQuery.filteredBrowsers - }), - groupedErrors - }; -} - -function closeDb(state) { - closeDatabase(state.db); - - return state; -} - -function addTestResult(state, action) { - const { - config: {errorPatterns}, - view: {viewMode, filteredBrowsers, testNameFilter, strictMatchFilter} - } = state; - const suites = clone(state.suites); - - [].concat(action.payload).forEach((suite) => { - const {suitePath, browserResult, browserId} = suite; - const test = findNode(suites, suitePath); - - if (!test) { - return; - } - - test.browsers.forEach((b) => { - if (b.name === browserId) { - assign(b, browserResult); - } - }); - setStatusForBranch(suites, suitePath); - forceUpdateSuiteData(suites, test); - }); - - const suiteIds = clone(state.suiteIds); - assign(suiteIds, {failed: getFailedSuiteIds(suites)}); - - const groupedErrors = groupErrors({suites, viewMode, errorPatterns, filteredBrowsers, testNameFilter, strictMatchFilter}); - - return assign({}, state, {suiteIds, suites, groupedErrors}); -} - -function updateTestState(state, action, testState) { - const suites = clone(state.suites); - const {suitePath, browserId} = action.payload; - const test = findNode(suites, suitePath); - - if (!test) { - return; - } - - test.browsers.forEach((b) => { - if (b.name === browserId) { - merge(b, {state: testState}); - } - }); - - return assign({}, state, {suites}); -} - -function updateStateResult(state, action) { - const suites = clone(state.suites); - const {suitePath, browserId, stateName, retryIndex, opened} = action.payload; - const test = findNode(suites, suitePath); - - if (!test) { - return; - } - - const bro = find(test.browsers, {name: browserId}); - - if (!bro) { - return; - } - - const broResult = bro.retries.concat(bro.result)[retryIndex]; - const stateResult = stateName ? find(broResult.imagesInfo, {stateName}) : last(broResult.imagesInfo); - - assign(stateResult, {opened}); - - return {...state, suites}; -} - -function _mutateStateView(state, mutation) { - const newView = clone(state.view); - assign(newView, mutation); - - return assign(clone(state), {view: newView}); -} - -function _mutateViewMode(state, viewMode) { - const { - suites, - config: {errorPatterns}, - view: {filteredBrowsers, testNameFilter, strictMatchFilter} - } = state; - const groupedErrors = groupErrors({suites, viewMode, errorPatterns, filteredBrowsers, testNameFilter, strictMatchFilter}); - - return { - ...state, - groupedErrors, - view: { - ...state.view, - viewMode - } - }; -} - -function _parseHost(baseHost) { - const parsedHost = url.parse(baseHost, false, true); - return { - host: parsedHost.slashes ? parsedHost.host : baseHost, - protocol: parsedHost.slashes ? parsedHost.protocol : null, - hostname: null, - port: null - }; -} - -function formatSuitesData(suites = []) { - return { - suites: reduce(suites, (acc, s) => { - acc[getSuiteId(s)] = s; - return acc; - }, {}), - suiteIds: { - all: getSuiteIds(suites), - failed: getFailedSuiteIds(suites) - } - }; -} - -function extractBrowsers(suite) { - const getVersion = ({result}) => ({ - id: result.name, - versions: _.get(result, 'metaInfo.browserVersion', BrowserVersions.UNKNOWN) - }); - const getVersions = (browsers = []) => browsers.map(getVersion); - const getUniqVersions = (set) => _(set) - .map('versions') - .compact() - .uniq() - .value(); - const iterate = (node) => [] - .concat(node.children) - .map((subNode = {}) => subNode.children ? iterate(subNode) : getVersions(subNode.browsers)); - - return _(suite) - .values(suite) - .map(iterate) - .flattenDeep() - .groupBy('id') - .mapValues(getUniqVersions) - .map((versions, id) => ({id, versions})) - .value(); -} - -function getFailedSuiteIds(suites) { - return getSuiteIds(filter(suites, isSuiteFailed)); -} - -function getSuiteIds(suites = []) { - return map(suites, getSuiteId); -} - -function getSuiteId(suite) { - return suite.suitePath[0]; -} - -/* - * To re-render suite we need to change object reference because of shallow data comparing - */ -function forceUpdateSuiteData(suites, test) { - const id = getSuiteId(test); - suites[id] = cloneDeep(suites[id]); -} - -export function withBrowserStorage(reducer) { - return (state, action) => { - const newState = reducer(state, action); - - if (/^VIEW_/.test(action.type)) { - const {view} = newState; - // do not save text inputs: - // for example, a user opens a new report and sees no tests in it - // as the filter is applied from the previous opening of another report - localStorageWrapper.setItem('view', { - expand: view.expand, - viewMode: view.viewMode, - showSkipped: view.showSkipped, - showOnlyDiff: view.showOnlyDiff, - scaleImages: view.scaleImages, - // TODO: Uncomment when issues with rendering speed will fixed - // lazyLoadOffset: view.lazyLoadOffset, - groupByError: view.groupByError, - strictMatchFilter: view.strictMatchFilter - }); - } - - return newState; - }; -} diff --git a/lib/static/modules/reducers/running.js b/lib/static/modules/reducers/running.js new file mode 100644 index 000000000..194c9ba00 --- /dev/null +++ b/lib/static/modules/reducers/running.js @@ -0,0 +1,19 @@ +import actionNames from '../action-names'; + +export default (state = {}, action) => { + switch (action.type) { + case actionNames.RUN_ALL_TESTS: + case actionNames.RUN_FAILED_TESTS: + case actionNames.RETRY_SUITE: + case actionNames.RETRY_TEST: { + return {...state, running: true}; + } + + case actionNames.TESTS_END: { + return {...state, running: false}; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/skips.js b/lib/static/modules/reducers/skips.js new file mode 100644 index 000000000..152dc0c1f --- /dev/null +++ b/lib/static/modules/reducers/skips.js @@ -0,0 +1,15 @@ +import actionNames from '../action-names'; + +export default (state, action) => { + switch (action.type) { + case actionNames.INIT_GUI_REPORT: + case actionNames.INIT_STATIC_REPORT: { + const {skips} = action.payload; + + return {...state, skips}; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/stats.js b/lib/static/modules/reducers/stats.js new file mode 100644 index 000000000..86b4e506e --- /dev/null +++ b/lib/static/modules/reducers/stats.js @@ -0,0 +1,21 @@ +import actionNames from '../action-names'; + +export default (state, action) => { + switch (action.type) { + case actionNames.INIT_STATIC_REPORT: { + const {stats} = action.payload; + const {perBrowser, ...restStats} = stats; + + return { + ...state, + stats: { + all: restStats, + perBrowser + } + }; + } + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/tree.js b/lib/static/modules/reducers/tree.js new file mode 100644 index 000000000..a596a5df1 --- /dev/null +++ b/lib/static/modules/reducers/tree.js @@ -0,0 +1,99 @@ +import {produce} from 'immer'; +import actionNames from '../action-names'; +import {isSuiteFailed} from '../utils'; + +export default produce((state, action) => { + switch (action.type) { + case actionNames.INIT_GUI_REPORT: + case actionNames.INIT_STATIC_REPORT: { + const {tree} = action.payload; + + state.tree = tree; + state.tree.suites.failedRootIds = getFailedRootSuiteIds(tree.suites); + + state.tree.browsers.stateById = {}; + state.tree.images.stateById = {}; + + break; + } + + case actionNames.RUN_ALL_TESTS: { + const {status} = action.payload; + + state.tree.suites.allIds.forEach((suiteId) => { + state.tree.suites.byId[suiteId].status = status; + }); + + break; + } + + case actionNames.SUITE_BEGIN: { + const {suiteId, status} = action.payload; + + state.tree.suites.byId[suiteId].status = status; + + break; + } + + case actionNames.TEST_BEGIN: + case actionNames.TEST_RESULT: + case actionNames.ACCEPT_OPENED_SCREENSHOTS: + case actionNames.ACCEPT_SCREENSHOT: { + [].concat(action.payload).forEach(({result, images, suites}) => { + state.tree.results.byId[result.id] = result; + if (!state.tree.results.allIds.includes(result.id)) { + state.tree.results.allIds.push(result.id); + } + + images.forEach((image) => { + state.tree.images.byId[image.id] = image; + if (!state.tree.images.allIds.includes(image.id)) { + state.tree.images.allIds.push(image.id); + } + }); + + if (!state.tree.browsers.byId[result.parentId].resultIds.includes(result.id)) { + state.tree.browsers.byId[result.parentId].resultIds.push(result.id); + } + + suites.forEach(({id, status}) => { + state.tree.suites.byId[id].status = status; + }); + + state.tree.suites.failedRootIds = getFailedRootSuiteIds(state.tree.suites); + }); + + break; + } + + case actionNames.CHANGE_TEST_RETRY: { + const {browserId, retryIndex} = action.payload; + + if (!state.tree.browsers.stateById[browserId]) { + state.tree.browsers.stateById[browserId] = {retryIndex}; + } else { + state.tree.browsers.stateById[browserId].retryIndex = retryIndex; + } + + break; + } + + case actionNames.TOGGLE_STATE_RESULT: { + const {imageId, opened} = action.payload; + + if (!state.tree.images.stateById[imageId]) { + state.tree.images.stateById[imageId] = {opened}; + } else { + state.tree.images.stateById[imageId].opened = opened; + } + + break; + } + } +}); + +function getFailedRootSuiteIds(suites) { + return suites.allRootIds.filter((rootId) => { + return isSuiteFailed(suites.byId[rootId]); + }); +} diff --git a/lib/static/modules/reducers/view.js b/lib/static/modules/reducers/view.js new file mode 100644 index 000000000..091127015 --- /dev/null +++ b/lib/static/modules/reducers/view.js @@ -0,0 +1,122 @@ +import url from 'url'; +import {produce} from 'immer'; +import {isEmpty} from 'lodash'; +import {getViewQuery} from '../custom-queries'; +import * as localStorageWrapper from '../local-storage-wrapper'; +import actionNames from '../action-names'; +import viewModes from '../../../constants/view-modes'; + +export default produce((state, action) => { + switch (action.type) { + case actionNames.INIT_GUI_REPORT: + case actionNames.INIT_STATIC_REPORT: { + const {scaleImages, lazyLoadOffset, baseHost, defaultView: viewMode} = state.config; + const viewQuery = getViewQuery(window.location.search); + const lsView = localStorageWrapper.getItem('view', {}); + + if (isEmpty(viewQuery.filteredBrowsers)) { + viewQuery.filteredBrowsers = state.browsers; + } + + state.view = {...state.view, scaleImages, lazyLoadOffset, viewMode, baseHost, ...lsView, ...viewQuery}; + + break; + } + + case actionNames.RUN_ALL_TESTS: + case actionNames.RUN_FAILED_TESTS: + case actionNames.RETRY_SUITE: + case actionNames.RETRY_TEST: { + state.view.groupByError = false; + break; + } + + case actionNames.VIEW_EXPAND_ALL: { + state.view.expand = 'all'; + break; + } + + case actionNames.VIEW_EXPAND_ERRORS: { + state.view.expand = 'errors'; + break; + } + + case actionNames.VIEW_EXPAND_RETRIES: { + state.view.expand = 'retries'; + break; + } + + case actionNames.VIEW_COLLAPSE_ALL: { + state.view.expand = 'none'; + break; + } + + case actionNames.VIEW_SHOW_ALL: { + state.view.viewMode = viewModes.ALL; + break; + } + + case actionNames.VIEW_SHOW_FAILED: { + state.view.viewMode = viewModes.FAILED; + break; + } + + case actionNames.VIEW_TOGGLE_SKIPPED: { + state.view.showSkipped = !state.view.showSkipped; + break; + } + + case actionNames.VIEW_TOGGLE_ONLY_DIFF: { + state.view.showOnlyDiff = !state.view.showOnlyDiff; + break; + } + + case actionNames.VIEW_TOGGLE_SCALE_IMAGES: { + state.view.scaleImages = !state.view.scaleImages; + break; + } + + case actionNames.VIEW_TOGGLE_LAZY_LOAD_IMAGES: { + state.view.lazyLoadOffset = state.view.lazyLoadOffset ? 0 : state.config.lazyLoadOffset; + break; + } + + case actionNames.VIEW_UPDATE_BASE_HOST: { + const baseHost = action.host; + state.view.baseHost = parseHost(baseHost); + + break; + } + + case actionNames.VIEW_UPDATE_FILTER_BY_NAME: { + state.view.testNameFilter = action.testNameFilter; + break; + } + + case actionNames.VIEW_SET_STRICT_MATCH_FILTER: { + state.view.strictMatchFilter = action.strictMatchFilter; + break; + } + + case actionNames.VIEW_TOGGLE_GROUP_BY_ERROR: { + state.view.groupByError = !state.view.groupByError; + break; + } + + case actionNames.BROWSERS_SELECTED: { + state.view.filteredBrowsers = action.payload.browsers; + break; + } + } +}); + +function parseHost(baseHost) { + const parsedHost = url.parse(baseHost, false, true); + + return { + host: parsedHost.slashes ? parsedHost.host : baseHost, + protocol: parsedHost.slashes ? parsedHost.protocol : null, + hostname: null, + port: null + }; +} diff --git a/lib/static/modules/selectors/stats.js b/lib/static/modules/selectors/stats.js new file mode 100644 index 000000000..456331a9e --- /dev/null +++ b/lib/static/modules/selectors/stats.js @@ -0,0 +1,33 @@ +import {createSelector} from 'reselect'; +import {isEmpty, keys, flatten, forEach} from 'lodash'; +import {getFilteredBrowsers} from './view'; + +const getStats = (state) => state.stats; + +export const getStatsFilteredByBrowsers = createSelector( + getStats, getFilteredBrowsers, + (stats, filteredBrowsers) => { + if (isEmpty(filteredBrowsers) || isEmpty(stats.perBrowser)) { + return stats.all; + } + + const resStats = {}; + const rows = filteredBrowsers.map((browserToFilterBy) => { + const {id, versions} = browserToFilterBy; + + return isEmpty(versions) + ? keys(stats.perBrowser[id]).map((ver) => stats.perBrowser[id][ver]) + : versions.map((ver) => stats.perBrowser[id][ver]); + }); + + flatten(rows).forEach((stats) => { + forEach(stats, (value, stat) => { + resStats[stat] = resStats[stat] === undefined + ? value + : resStats[stat] + value; + }); + }); + + return resStats; + } +); diff --git a/lib/static/modules/selectors/tree.js b/lib/static/modules/selectors/tree.js new file mode 100644 index 000000000..760a13049 --- /dev/null +++ b/lib/static/modules/selectors/tree.js @@ -0,0 +1,213 @@ +import {last, initial, identity, flatMap, isEmpty, find, compact} from 'lodash'; +import {createSelector} from 'reselect'; +import {getFilteredBrowsers, getTestNameFilter, getStrictMatchFilter, getViewMode} from './view'; +import viewModes from '../../../constants/view-modes'; +import {isFailStatus, isErroredStatus} from '../../../common-utils'; +import {isNodeFailed, isAcceptable} from '../utils'; + +const getSuites = (state) => state.tree.suites.byId; +const getBrowsers = (state) => state.tree.browsers.byId; +const getResults = (state) => state.tree.results.byId; +const getImages = (state) => state.tree.images.byId; +const getFailedRootSuiteIds = (state) => state.tree.suites.failedRootIds; +const getImagesStates = (state) => state.tree.images.stateById; +const getRootSuiteIds = (state) => { + const viewMode = getViewMode(state); + return viewMode === viewModes.FAILED ? state.tree.suites.failedRootIds : state.tree.suites.allRootIds; +}; + +const getSuiteById = (state, {suiteId}) => state.tree.suites.byId[suiteId]; +const getBrowserById = (state, {browserId}) => state.tree.browsers.byId[browserId]; +const getResultStatus = (state, {result}) => result.status; +const getErrorGroupBrowserIds = (state, {errorGroupBrowserIds} = {}) => errorGroupBrowserIds; + +export const getFailedTests = createSelector( + getSuites, getBrowsers, getResults, getFailedRootSuiteIds, + (suites, browsers, results, failedRootSuiteIds) => { + const lastResults = flatMap(failedRootSuiteIds, (suiteId) => getSuiteResults(suites[suiteId], {suites, browsers, results}, last)); + const failedLastResults = lastResults.filter((result) => isNodeFailed(result)); + + return failedLastResults.map((result) => { + const browserId = result.parentId; + const {parentId: testName, name: browserName} = browsers[browserId]; + + return {testName, browserName}; + }); + } +); + +export const mkGetTestsBySuiteId = () => createSelector( + getSuiteById, getSuites, getBrowsers, + (suite, suites, browsers) => { + const suiteBrowsers = getSuiteBrowsers(suite, {suites, browsers}); + + return suiteBrowsers.map(({parentId, name}) => ({testName: parentId, browserName: name})); + } +); + +export const getOpenedImageIds = createSelector( + getImagesStates, + (imagesStates) => Object.keys(imagesStates).filter((imgId) => imagesStates[imgId].opened) +); + +export const getAcceptableOpenedImageIds = createSelector( + getOpenedImageIds, getImages, + (openedImageIds, images) => { + return openedImageIds.map((imgId) => images[imgId]).filter(isAcceptable).map((image) => image.id); + } +); + +export const getFailedOpenedImageIds = createSelector( + getOpenedImageIds, getImages, + (openedImageIds, images) => { + return openedImageIds.map((imgId) => images[imgId]).filter(isNodeFailed).map((image) => image.id); + } +); + +export const mkGetVisibleRootSuiteIds = () => createSelector( + getRootSuiteIds, getSuites, getBrowsers, getErrorGroupBrowserIds, + getTestNameFilter, getStrictMatchFilter, getFilteredBrowsers, getViewMode, + (rootSuiteIds, suites, ...args) => { + return rootSuiteIds.filter((suiteId) => shouldSuiteBeShown(suites[suiteId], suites, ...args)); + } +); + +export const mkHasSuiteFailedRetries = () => createSelector( + getSuiteById, getSuites, getBrowsers, getResults, + (suite, suites, browsers, results) => { + const retries = getSuiteResults(suite, {suites, browsers, results}, initial); + + return retries.some((retry) => isNodeFailed(retry)); + } +); + +export const mkHasBrowserFailedRetries = () => createSelector( + getBrowserById, getResults, + (browser, results) => { + const retries = [].concat(initial(browser.resultIds)).map((resultId) => results[resultId]); + + return retries.some((retry) => isNodeFailed(retry)); + } +); + +export const mkShouldSuiteBeShown = () => createSelector( + getSuiteById, getSuites, getBrowsers, getErrorGroupBrowserIds, + getTestNameFilter, getStrictMatchFilter, getFilteredBrowsers, getViewMode, + shouldSuiteBeShown +); + +function shouldSuiteBeShown(suite, suites, browsers, errorGroupBrowserIds, testNameFilter, strictMatchFilter, filteredBrowsers, viewMode) { + if (!isStatusMatchViewMode(suite.status, viewMode)) { + return false; + } + + const suiteBrowsers = getSuiteBrowsers(suite, {suites, browsers}); + + return suiteBrowsers.some((browser) => { + const testName = browser.parentId; + + if (!isTestNameMatchFilters(testName, testNameFilter, strictMatchFilter)) { + return false; + } + + return shouldShowBrowser(browser, filteredBrowsers, errorGroupBrowserIds); + }); +} + +export const mkShouldBrowserBeShown = () => createSelector( + getBrowserById, getResultStatus, getErrorGroupBrowserIds, getFilteredBrowsers, getViewMode, + (browser, lastResultStatus, errorGroupBrowserIds, filteredBrowsers, viewMode) => { + if (!isStatusMatchViewMode(lastResultStatus, viewMode)) { + return false; + } + + return shouldShowBrowser(browser, filteredBrowsers, errorGroupBrowserIds); + } +); + +function getSuiteResults(node, tree, filterFn = identity) { + const {suites, browsers, results} = tree; + + if (node.resultIds) { + return [].concat(filterFn(node.resultIds)).map((resultId) => results[resultId]); + } + + if (node.browserIds) { + return flatMap(node.browserIds, (browserId) => { + return getSuiteResults(browsers[browserId], tree, filterFn); + }); + } + + return flatMap(node.suiteIds, (suiteId) => getSuiteResults(suites[suiteId], tree, filterFn)); +} + +function getSuiteBrowsers(suite, tree) { + const {suites, browsers} = tree; + + if (suite.browserIds) { + return suite.browserIds.map((browserId) => browsers[browserId]); + } + + return flatMap(suite.suiteIds, (suiteId) => getSuiteBrowsers(suites[suiteId], tree)); +} + +function isStatusMatchViewMode(status, viewMode) { + if (viewMode === viewModes.ALL) { + return true; + } + + if (viewMode === viewModes.FAILED && isFailStatus(status) || isErroredStatus(status)) { + return true; + } + + return false; +} + +function isTestNameMatchFilters(testName, testNameFilter, strictMatchFilter) { + if (!testNameFilter) { + return true; + } + + return strictMatchFilter ? testName === testNameFilter : testName.includes(testNameFilter); +} + +export function shouldShowBrowser(browser, filteredBrowsers, errorGroupBrowserIds = []) { + if (isEmpty(filteredBrowsers) && isEmpty(errorGroupBrowserIds)) { + return true; + } + + const browserToFilterBy = filteredBrowsers.length === 0 || find(filteredBrowsers, {id: browser.name}); + const matchErrorGroup = errorGroupBrowserIds.length === 0 || errorGroupBrowserIds.includes(browser.id); + + if (!browserToFilterBy || !matchErrorGroup) { + return false; + } + + const browserVersionsToFilterBy = [].concat(browserToFilterBy.versions).filter(Boolean); + + if (isEmpty(browserVersionsToFilterBy)) { + return true; + } + + return browser.versions.some((browserVersion) => browserVersionsToFilterBy.includes(browserVersion)); +} + +export function getFailedSuiteResults(tree) { + const failedRootSuites = tree.suites.failedRootIds.map((suiteId) => tree.suites.byId[suiteId]); + const failedTestSuites = compact(flatMap(failedRootSuites, (failedRootSuite) => getFailedTestSuites(failedRootSuite, tree.suites.byId))); + const preparedTree = {suites: tree.suites.byId, browsers: tree.browsers.byId, results: tree.results.byId}; + + return flatMap(failedTestSuites, (suite) => getSuiteResults(suite, preparedTree)); +} + +function getFailedTestSuites(suite, suites) { + if (!isNodeFailed(suite)) { + return; + } + + if (suite.browserIds) { + return suite; + } + + return flatMap(suite.suiteIds, (suiteId) => getFailedTestSuites(suites[suiteId], suites)); +} diff --git a/lib/static/modules/selectors/view.js b/lib/static/modules/selectors/view.js new file mode 100644 index 000000000..d898bef80 --- /dev/null +++ b/lib/static/modules/selectors/view.js @@ -0,0 +1,4 @@ +export const getFilteredBrowsers = (state) => state.view.filteredBrowsers; +export const getTestNameFilter = (state) => state.view.testNameFilter; +export const getStrictMatchFilter = (state) => state.view.strictMatchFilter; +export const getViewMode = (state) => state.view.viewMode; diff --git a/lib/static/modules/store.js b/lib/static/modules/store.js index 2e04695ef..b06e557c3 100644 --- a/lib/static/modules/store.js +++ b/lib/static/modules/store.js @@ -6,8 +6,9 @@ import logger from 'redux-logger'; import reducer from './reducers'; import metrika from './middlewares/metrika'; import YandexMetrika from './yandex-metrika'; +import localStorage from './middlewares/local-storage'; -const middlewares = [thunk, metrika(YandexMetrika)]; +const middlewares = [thunk, metrika(YandexMetrika), localStorage]; if (process.env.NODE_ENV !== 'production') { middlewares.push(logger); diff --git a/lib/static/modules/utils.js b/lib/static/modules/utils.js index efa60fbb0..d2597fe86 100644 --- a/lib/static/modules/utils.js +++ b/lib/static/modules/utils.js @@ -1,21 +1,8 @@ 'use strict'; -const {config: {defaultView}} = require('../../constants/defaults'); -const viewModes = require('../../constants/view-modes'); -const {versions: BrowserVersions} = require('../../constants/browser'); - const url = require('url'); -const {isArray, isObject, find, get, values, isEmpty, forEach, flatten, keys} = require('lodash'); - -const { - isIdleStatus, - isSuccessStatus, - isFailStatus, - isErroredStatus, - isSkippedStatus, - determineStatus -} = require('../../common-utils'); - +const {get, isEmpty} = require('lodash'); +const {isIdleStatus, isSuccessStatus, isFailStatus, isErroredStatus, isSkippedStatus} = require('../../common-utils'); const {getCommonErrors} = require('../../constants/errors'); const {NO_REF_IMAGE_ERROR} = getCommonErrors(); @@ -35,14 +22,8 @@ function hasNoRefImageErrors({imagesInfo = []}) { return Boolean(imagesInfo.filter(({error}) => isNoRefImageError(error)).length); } -function hasFails(node) { - const {result} = node; - - const isFailed = result && ( - hasFailedImages(result) || isErroredStatus(result.status) || isFailStatus(result.status) - ); - - return isFailed || walk(node, hasFails); +function hasResultFails(testResult) { + return hasFailedImages(testResult) || isErroredStatus(testResult.status) || isFailStatus(testResult.status); } function isSuiteIdle(suite) { @@ -57,117 +38,12 @@ function isSuiteFailed(suite) { return isFailStatus(suite.status) || isErroredStatus(suite.status); } -function isAcceptable({status, error}) { - return isErroredStatus(status) && isNoRefImageError(error) || isFailStatus(status) || isSkippedStatus(status); -} - -function hasFailedRetries(node) { - // TODO: remove filtering out nulls in retries after fix - const isRetried = (node.retries || []).filter(isObject).some(isSuiteFailed); - return isRetried || walk(node, hasFailedRetries); -} - -function allSkipped(node) { - const {result} = node; - const isSkipped = result && isSkippedStatus(result.status); - - return Boolean(isSkipped || walk(node, allSkipped, Array.prototype.every)); -} - -function walk(node, cb, fn = Array.prototype.some) { - return node.browsers && fn.call(node.browsers, cb) || node.children && fn.call(node.children, cb); +function isNodeFailed(node) { + return isFailStatus(node.status) || isErroredStatus(node.status); } -function setStatusToAll(node, status) { - if (isArray(node)) { - node.forEach((n) => setStatusToAll(n, status)); - } - - const currentStatus = get(node, 'result.status', node.status); - if (isSkippedStatus(currentStatus)) { - return; - } - node.result - ? (node.result.status = status) - : node.status = status; - - return walk(node, (n) => setStatusToAll(n, status), Array.prototype.forEach); -} - -function findNode(node, suitePath) { - suitePath = suitePath.slice(); - - if (!node.children) { - node = values(node); - const tree = { - name: 'root', - children: node - }; - return findNode(tree, suitePath); - } - - const pathPart = suitePath.shift(); - const child = find(node.children, {name: pathPart}); - - if (!child) { - return; - } - - if (child.name === pathPart && !suitePath.length) { - return child; - } - - return findNode(child, suitePath); -} - -function setStatusForBranch(nodes, suitePath) { - const node = findNode(nodes, suitePath); - if (!node) { - return; - } - - const statusesBrowser = node.browsers - ? node.browsers.map(({result: {status}}) => status) - : []; - - const statusesChildren = node.children - ? node.children.map(({status}) => status) - : []; - - const status = determineStatus([...statusesBrowser, ...statusesChildren]); - - // if newly determined status is the same as current status, do nothing - if (node.status === status) { - return; - } - - node.status = status; - setStatusForBranch(nodes, suitePath.slice(0, -1)); -} - -function getStats(stats, filteredBrowsers) { - if (isEmpty(filteredBrowsers) || isEmpty(stats.perBrowser)) { - return stats.all; - } - - const resStats = {}; - const rows = filteredBrowsers.map((browserToFilterBy) => { - const {id, versions} = browserToFilterBy; - - return isEmpty(versions) - ? keys(stats.perBrowser[id]).map((ver) => stats.perBrowser[id][ver]) - : versions.map((ver) => stats.perBrowser[id][ver]); - }); - - flatten(rows).forEach((stats) => { - forEach(stats, (value, stat) => { - resStats[stat] = resStats[stat] === undefined - ? value - : resStats[stat] + value; - }); - }); - - return resStats; +function isAcceptable({status, error}) { + return isErroredStatus(status) && isNoRefImageError(error) || isFailStatus(status) || isSkippedStatus(status); } function dateToLocaleString(date) { @@ -178,157 +54,6 @@ function dateToLocaleString(date) { return new Date(date).toLocaleString(lang); } -function isStatusMatchViewMode(status, viewMode) { - if (viewMode === viewModes.ALL) { - return true; - } - - if (viewMode === viewModes.FAILED && isFailStatus(status) || isErroredStatus(status)) { - return true; - } - - return false; -} - -function shouldSuiteBeShown({ - suite, - testNameFilter = '', - strictMatchFilter = false, - filteredBrowsers = [], - errorGroupTests = {}, - viewMode = defaultView -}) { - if (!isStatusMatchViewMode(suite.status, viewMode)) { - return false; - } - - const strictTestNameFilters = Object.keys(errorGroupTests); - - // suite may contain children and browsers - if (suite.hasOwnProperty('children')) { - const shouldChildrenBeShown = suite.children.some(child => - shouldSuiteBeShown({ - suite: child, - testNameFilter, - strictMatchFilter, - errorGroupTests, - filteredBrowsers, - viewMode - }) - ); - - if (shouldChildrenBeShown) { - return true; - } - } - - if (!suite.hasOwnProperty('browsers')) { - return false; - } - - const suiteFullPath = suite.suitePath.join(' '); - const matchName = isTestNameMatchFilters(suiteFullPath, testNameFilter, strictMatchFilter); - const strictMatchNames = strictTestNameFilters.length === 0 || strictTestNameFilters.includes(suiteFullPath); - const shouldShowSuite = () => { - if (isEmpty(filteredBrowsers)) { - return true; - } - - return suite.browsers.some((browser) => { - const browserResultStatus = get(browser, 'result.status'); - const shouldShowForViewMode = isStatusMatchViewMode(browserResultStatus, viewMode); - - if (!shouldShowForViewMode) { - return false; - } - - return shouldShowBrowser(browser, filteredBrowsers); - }); - }; - - return matchName && strictMatchNames && shouldShowSuite(); -} - -function shouldBrowserBeShown({ - browser, - fullTestName, - filteredBrowsers = [], - errorGroupTests = {}, - viewMode = defaultView -}) { - if (!isStatusMatchViewMode(get(browser, 'result.status'), viewMode)) { - return false; - } - - const {name} = browser; - let errorGroupBrowsers = []; - - if (errorGroupTests && errorGroupTests.hasOwnProperty(fullTestName)) { - errorGroupBrowsers = errorGroupTests[fullTestName]; - } - - const matchErrorGroupBrowsers = errorGroupBrowsers.length === 0 || errorGroupBrowsers.includes(name); - - return shouldShowBrowser(browser, filteredBrowsers) && matchErrorGroupBrowsers; -} - -function shouldShowBrowser(browser, filteredBrowsers) { - if (isEmpty(filteredBrowsers)) { - return true; - } - - const browserToFilterBy = find(filteredBrowsers, {id: browser.name}); - - if (!browserToFilterBy) { - return false; - } - - const browserVersionsToFilterBy = [] - .concat(browserToFilterBy.versions) - .filter(Boolean); - - if (isEmpty(browserVersionsToFilterBy)) { - return true; - } - - const browserVersion = get(browser, 'result.metaInfo.browserVersion', BrowserVersions.UNKNOWN); - - return browserVersionsToFilterBy.includes(browserVersion); -} - -function filterSuites(suites = [], filteredBrowsers = []) { - if (isEmpty(filteredBrowsers) || isEmpty(suites)) { - return suites; - } - - const filteredSuites = suites.filter((suite) => { - let result = false; - let browserStatuses = []; - - if (suite.browsers) { - suite.browsers = suite.browsers.filter(({name}) => filteredBrowsers.includes(name)); - browserStatuses = suite.browsers.map(({result: {status}}) => status); - - result = result || suite.browsers.length > 0; - } - - let childrenStatuses = []; - - if (suite.children) { - suite.children = filterSuites(suite.children, filteredBrowsers); - childrenStatuses = suite.children.map(({status}) => status); - - result = result || suite.children.length > 0; - } - - suite.status = determineStatus([...browserStatuses, ...childrenStatuses]); - - return result; - }); - - return filteredSuites; -} - function isUrl(str) { if (typeof str !== 'string') { return false; @@ -358,23 +83,14 @@ function getHttpErrorMessage(error) { module.exports = { isNoRefImageError, hasNoRefImageErrors, - hasFails, + hasResultFails, isSuiteIdle, isSuiteSuccessful, isSuiteFailed, + isNodeFailed, isAcceptable, - hasFailedRetries, - allSkipped, - findNode, - setStatusToAll, - setStatusForBranch, - getStats, dateToLocaleString, - shouldSuiteBeShown, - shouldBrowserBeShown, isUrl, - filterSuites, isTestNameMatchFilters, - getHttpErrorMessage, - shouldShowBrowser + getHttpErrorMessage }; diff --git a/lib/test-adapter.js b/lib/test-adapter.js index 38000f32e..8a873eff0 100644 --- a/lib/test-adapter.js +++ b/lib/test-adapter.js @@ -240,6 +240,10 @@ module.exports = class TestAdapter { return this._suite.path.concat(this._testResult.title); } + get id() { + return this.testPath.concat(this.browserId, this.attempt).join(' '); + } + get screenshot() { return _.get(this._testResult, 'err.screenshot'); } diff --git a/lib/tests-tree-builder/base.js b/lib/tests-tree-builder/base.js index b9dea551d..9ad439cdd 100644 --- a/lib/tests-tree-builder/base.js +++ b/lib/tests-tree-builder/base.js @@ -2,6 +2,7 @@ const _ = require('lodash'); const {determineStatus} = require('../common-utils'); +const {versions: browserVersions} = require('../constants/browser'); module.exports = class ResultsTreeBuilder { static create() { @@ -40,6 +41,7 @@ module.exports = class ResultsTreeBuilder { addTestResult(testResult, formattedResult) { const {testPath, browserId: browserName, attempt} = formattedResult; const {imagesInfo} = testResult; + const {browserVersion = browserVersions.UNKNOWN} = testResult.metaInfo; const suiteId = this._buildId(testPath); const browserId = this._buildId(suiteId, browserName); @@ -48,7 +50,7 @@ module.exports = class ResultsTreeBuilder { .map((image, i) => this._buildId(testResultId, image.stateName || `${image.status}_${i}`)); this._addSuites(testPath, browserId); - this._addBrowser({id: browserId, parentId: suiteId, name: browserName}, testResultId, attempt); + this._addBrowser({id: browserId, parentId: suiteId, name: browserName, version: browserVersion}, testResultId, attempt); this._addResult({id: testResultId, parentId: browserId, result: testResult}, imageIds); this._addImages(imageIds, {imagesInfo, parentId: testResultId}); @@ -74,9 +76,9 @@ module.exports = class ResultsTreeBuilder { if (ind !== arr.length - 1) { const childSuiteId = this._buildId(id, arr[ind + 1]); - this._addChildSuiteId(id, childSuiteId); + this._addNodeId(id, childSuiteId, {fieldName: 'suiteIds'}); } else { - this._addBrowserId(id, browserId); + this._addNodeId(id, browserId, {fieldName: 'browserIds'}); } return suites; @@ -94,55 +96,45 @@ module.exports = class ResultsTreeBuilder { } } - _addChildSuiteId(parentSuiteId, childSuiteId) { + _addNodeId(parentSuiteId, nodeId, {fieldName}) { const {suites} = this._tree; - if (!suites.byId[parentSuiteId].suiteIds) { - suites.byId[parentSuiteId].suiteIds = [childSuiteId]; + if (!suites.byId[parentSuiteId][fieldName]) { + suites.byId[parentSuiteId][fieldName] = [nodeId]; return; } - if (!this._isChildSuiteIdExists(parentSuiteId, childSuiteId)) { - suites.byId[parentSuiteId].suiteIds.push(childSuiteId); + if (!this._isNodeIdExists(parentSuiteId, nodeId, {fieldName})) { + suites.byId[parentSuiteId][fieldName].push(nodeId); } } - _isChildSuiteIdExists(parentSuiteId, childSuiteId) { - return _.includes(this._tree.suites.byId[parentSuiteId].suiteIds, childSuiteId); + _isNodeIdExists(parentSuiteId, nodeId, {fieldName}) { + return _.includes(this._tree.suites.byId[parentSuiteId][fieldName], nodeId); } - _addBrowserId(parentSuiteId, browserId) { - const {suites} = this._tree; - - if (!suites.byId[parentSuiteId].browserIds) { - suites.byId[parentSuiteId].browserIds = [browserId]; - return; - } - - if (!this._isBrowserIdExists(parentSuiteId, browserId)) { - suites.byId[parentSuiteId].browserIds.push(browserId); - } - } - - _isBrowserIdExists(parentSuiteId, browserId) { - return _.includes(this._tree.suites.byId[parentSuiteId].browserIds, browserId); - } - - _addBrowser({id, parentId, name}, testResultId, attempt) { + _addBrowser({id, parentId, name, version}, testResultId, attempt) { const {browsers} = this._tree; if (!browsers.byId[id]) { - browsers.byId[id] = {id, parentId, name, resultIds: []}; + browsers.byId[id] = {id, parentId, name, resultIds: [], versions: []}; browsers.allIds.push(id); } this._addResultIdToBrowser(id, testResultId, attempt); + this._addBrowserVersion(id, version); } _addResultIdToBrowser(browserId, testResultId, attempt) { this._tree.browsers.byId[browserId].resultIds[attempt] = testResultId; } + _addBrowserVersion(browserId, browserVersion) { + if (!this._tree.browsers.byId[browserId].versions.includes(browserVersion)) { + this._tree.browsers.byId[browserId].versions.push(browserVersion); + } + } + _addResult({id, parentId, result}, imageIds) { const resultWithoutImagesInfo = _.omit(result, 'imagesInfo'); @@ -155,7 +147,7 @@ module.exports = class ResultsTreeBuilder { _addImages(imageIds, {imagesInfo, parentId}) { imageIds.forEach((id, ind) => { - this._tree.images.byId[id] = {id, parentId, ...imagesInfo[ind]}; + this._tree.images.byId[id] = {...imagesInfo[ind], id, parentId}; this._tree.images.allIds.push(id); }); } @@ -190,76 +182,4 @@ module.exports = class ResultsTreeBuilder { suite.status = status; this._setStatusForBranch(testPath.slice(0, -1)); } - - convertToOldFormat() { - const tree = {children: []}; - const {suites} = this._tree; - - suites.allRootIds.forEach((rootSuiteId) => { - const suite = this._convertSuiteToOldFormat(_.clone(suites.byId[rootSuiteId])); - tree.children.push(suite); - }); - - return {suites: tree.children}; - } - - _convertSuiteToOldFormat(suite) { - if (suite.suiteIds) { - suite.children = suite.suiteIds.map((childSuiteId) => { - const childSuite = _.clone(this._tree.suites.byId[childSuiteId]); - const result = this._convertSuiteToOldFormat(childSuite); - - return result; - }); - } - - if (suite.browserIds) { - suite.browsers = suite.browserIds.map((browserId) => { - const browser = _.clone(this._tree.browsers.byId[browserId]); - return this._convertBrowserToOldFormat(browser); - }); - } - - delete suite.suiteIds; - delete suite.browserIds; - delete suite.id; - delete suite.parentId; - delete suite.root; - - return suite; - } - - _convertBrowserToOldFormat(browser) { - browser.retries = browser.resultIds.slice(0, -1).map((resultId) => { - const result = _.clone(this._tree.results.byId[resultId]); - return this._convertImagesToOldFormat(result); - }); - - const resultId = _.last(browser.resultIds); - browser.result = _.clone(this._tree.results.byId[resultId]); - this._convertImagesToOldFormat(browser.result); - - delete browser.resultIds; - delete browser.id; - delete browser.parentId; - - return browser; - } - - _convertImagesToOldFormat(result) { - result.imagesInfo = result.imageIds.map((imageId) => { - const image = _.clone(this._tree.images.byId[imageId]); - - delete image.id; - delete image.parentId; - - return image; - }); - - delete result.imageIds; - delete result.id; - delete result.parentId; - - return result; - } }; diff --git a/lib/tests-tree-builder/gui.js b/lib/tests-tree-builder/gui.js index a011ef409..9ce20951a 100644 --- a/lib/tests-tree-builder/gui.js +++ b/lib/tests-tree-builder/gui.js @@ -2,6 +2,7 @@ const _ = require('lodash'); const BaseTestsTreeBuilder = require('./base'); +const {UPDATED} = require('../constants/test-statuses'); module.exports = class GuiTestsTreeBuilder extends BaseTestsTreeBuilder { getLastResult(formattedResult) { @@ -14,17 +15,70 @@ module.exports = class GuiTestsTreeBuilder extends BaseTestsTreeBuilder { return this._tree.results.byId[testResultId]; } - getImagesInfo(formattedResult) { - const {testPath, browserId: browserName, attempt} = formattedResult; - const suiteId = this._buildId(testPath); - const browserId = this._buildId(suiteId, browserName); - const testResultId = this._buildId(browserId, attempt); - - return this._tree.results.byId[testResultId].imageIds.map((imageId) => { + getImagesInfo(testId) { + return this._tree.results.byId[testId].imageIds.map((imageId) => { return this._tree.images.byId[imageId]; }); } + getTestBranch(id) { + const getSuites = (suite) => { + if (suite.root) { + return {id: suite.id, status: suite.status}; + } + + return _.flatten([ + getSuites(this._tree.suites.byId[suite.parentId]), + {id: suite.id, status: suite.status} + ]); + }; + + const result = this._tree.results.byId[id]; + const images = result.imageIds.map((imgId) => this._tree.images.byId[imgId]); + const browser = this._tree.browsers.byId[result.parentId]; + const suites = getSuites(this._tree.suites.byId[browser.parentId]); + + return {result, images, suites}; + } + + getTestsDataToUpdateRefs(imageIds) { + const imagesById = [].concat(imageIds).reduce((acc, imgId) => { + acc[imgId] = this._tree.images.byId[imgId]; + + return acc; + }, {}); + + const imagesByResultId = _.groupBy(imagesById, 'parentId'); + + return Object.keys(imagesByResultId).map((resultId) => { + const result = this._tree.results.byId[resultId]; + const browser = this._tree.browsers.byId[result.parentId]; + const suite = this._tree.suites.byId[browser.parentId]; + + const imagesInfo = imagesByResultId[resultId] + .map(({stateName, actualImg}) => ({stateName, actualImg, status: UPDATED})); + + return { + suite: {path: suite.suitePath.slice(0, -1)}, + state: {name: suite.name}, + browserId: browser.name, + metaInfo: result.metaInfo, + imagesInfo, + attempt: result.attempt + }; + }); + } + + getImageDataToFindEqualDiffs(imageIds) { + return imageIds.map((imageId) => { + const image = this._tree.images.byId[imageId]; + const result = this._tree.results.byId[image.parentId]; + const {name: browserName} = this._tree.browsers.byId[result.parentId]; + + return {...image, browserName}; + }); + } + reuseTestsTree(testsTree) { this._tree.browsers.allIds.forEach((browserId) => this._reuseBrowser(testsTree, browserId)); } diff --git a/lib/tests-tree-builder/static.js b/lib/tests-tree-builder/static.js index 558cd6c6c..ad1f71ea9 100644 --- a/lib/tests-tree-builder/static.js +++ b/lib/tests-tree-builder/static.js @@ -19,7 +19,7 @@ module.exports = class StaticTestsTreeBuilder extends BaseTestsTreeBuilder { this._passedBrowserIds = {}; } - build(rows = [], opts = {convertToOldFormat: true}) { + build(rows = []) { // in order to sync attempts between gui tree and static tree const attemptsMap = new Map(); const browsers = {}; @@ -46,7 +46,7 @@ module.exports = class StaticTestsTreeBuilder extends BaseTestsTreeBuilder { this.sortTree(); return { - tree: opts.convertToOldFormat ? this.convertToOldFormat() : this.tree, + tree: this.tree, stats: this._stats, skips: this._skips, browsers: _.map(browsers, (versions, id) => ({id, versions: Array.from(versions)})) diff --git a/package-lock.json b/package-lock.json index 4e4a0807e..c9e175eae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8493,6 +8493,11 @@ "dev": true, "optional": true }, + "immer": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.7.tgz", + "integrity": "sha512-Q8yYwVADJXrNfp1ZUAh4XDHkcoE3wpdpb4mC5abDSajs2EbW8+cGdPyAnglMyLnm7EF6ojD2xBFX7L5i4TIytw==" + }, "import-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", @@ -13337,6 +13342,11 @@ } } }, + "reduce-reducers": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reduce-reducers/-/reduce-reducers-1.0.4.tgz", + "integrity": "sha512-Mb2WZ2bJF597exiqX7owBzrqJ74DHLK3yOQjCyPAaNifRncE8OD0wFIuoMhXxTnHK07+8zZ2SJEKy/qtiyR7vw==" + }, "redux": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", @@ -13690,6 +13700,11 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" + }, "resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", diff --git a/package.json b/package.json index 6500b1724..144d41da4 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "gemini-configparser": "^1.0.0", "gemini-core": "^5.3.0", "http-codes": "1.0.0", + "immer": "^7.0.7", "json-stringify-safe": "^5.0.1", "lodash": "^4.17.4", "looks-same": "^7.1.1", @@ -61,6 +62,8 @@ "react-markdown": "^3.2.0", "reapop": "2.1.0", "reapop-theme-wybo": "1.0.2", + "reduce-reducers": "^1.0.4", + "reselect": "^4.0.0", "resolve": "^1.5.0", "semver": "^5.5.0", "shelljs": "^0.8.1", diff --git a/test/unit/lib/common-utils.js b/test/unit/lib/common-utils.js index ecd39019f..078225ea3 100644 --- a/test/unit/lib/common-utils.js +++ b/test/unit/lib/common-utils.js @@ -1,20 +1,9 @@ 'use strict'; -const sinon = require('sinon'); const {determineStatus} = require('lib/common-utils'); const {RUNNING, QUEUED, ERROR, FAIL, UPDATED, SUCCESS, IDLE, SKIPPED} = require('lib/constants/test-statuses'); describe('common-utils', () => { - let sandbox; - - beforeEach(() => { - sandbox = sinon.sandbox.create(); - }); - - afterEach(() => { - sandbox.restore(); - }); - describe('determineStatus', () => { it(`should not rewrite suite status to "${IDLE}" if some test already has final status`, () => { const status = determineStatus([SUCCESS, IDLE]); diff --git a/test/unit/lib/gui/app.js b/test/unit/lib/gui/app.js index fda18d332..8cab0d177 100644 --- a/test/unit/lib/gui/app.js +++ b/test/unit/lib/gui/app.js @@ -1,12 +1,11 @@ 'use strict'; -const path = require('path'); const _ = require('lodash'); const proxyquire = require('proxyquire'); const Promise = require('bluebird'); const ToolRunner = require('lib/gui/tool-runner'); -const {stubTool, stubConfig, mkImagesInfo} = require('../../utils'); +const {stubTool, stubConfig} = require('../../utils'); describe('lib/gui/app', () => { const sandbox = sinon.createSandbox().usingPromise(Promise); @@ -33,7 +32,8 @@ describe('lib/gui/app', () => { run: sandbox.stub().named('run').resolves(), finalize: sandbox.stub().named('finalize'), config: tool.config, - initialize: sandbox.stub().named('initialize').resolves() + initialize: sandbox.stub().named('initialize').resolves(), + findEqualDiffs: sandbox.stub().named('findEqualDiffs').resolves() }; }; @@ -112,99 +112,12 @@ describe('lib/gui/app', () => { }); describe('findEqualDiffs', () => { - let pluginConfig; - let compareOpts; - - beforeEach(() => { - tool = stubTool(stubConfig({tolerance: 100500, antialiasingTolerance: 500100})); - toolRunner = mkToolRunner_(tool); - ToolRunner.create.returns(toolRunner); - - pluginConfig = {path: 'report-path'}; - compareOpts = { - tolerance: 100500, - antialiasingTolerance: 500100, - stopOnFirstFail: true, - shouldCluster: false - }; - - sandbox.stub(path, 'resolve'); - }); - - it('should stop comparison on first diff in reference images', async () => { - const refImagesInfo = mkImagesInfo({expectedImg: {path: 'ref-path-1'}}); - const comparedImagesInfo = [mkImagesInfo({expectedImg: {path: 'ref-path-2'}})]; - - path.resolve - .withArgs(process.cwd(), pluginConfig.path, 'ref-path-1').returns('/ref-path-1') - .withArgs(process.cwd(), pluginConfig.path, 'ref-path-2').returns('/ref-path-2'); - - looksSame.withArgs( - {source: '/ref-path-1', boundingBox: refImagesInfo.diffClusters[0]}, - {source: '/ref-path-2', boundingBox: comparedImagesInfo[0].diffClusters[0]}, - compareOpts - ).yields(null, {equal: false}); - - const App_ = await mkApp_({tool, configs: {pluginConfig}}); - const result = await App_.findEqualDiffs([refImagesInfo].concat(comparedImagesInfo)); - - assert.calledOnce(looksSame); - assert.isEmpty(result); - }); - - it('should stop comparison on diff in actual images', async () => { - const refImagesInfo = mkImagesInfo({actualImg: {path: 'act-path-1'}}); - const comparedImagesInfo = [mkImagesInfo({actualImg: {path: 'act-path-2'}})]; - - path.resolve - .withArgs(process.cwd(), pluginConfig.path, 'act-path-1').returns('/act-path-1') - .withArgs(process.cwd(), pluginConfig.path, 'act-path-2').returns('/act-path-2'); - - looksSame.onFirstCall().yields(null, {equal: true}); - looksSame.withArgs( - {source: '/act-path-1', boundingBox: refImagesInfo.diffClusters[0]}, - {source: '/act-path-2', boundingBox: comparedImagesInfo[0].diffClusters[0]}, - compareOpts - ).yields(null, {equal: false}); - - const App_ = await mkApp_({tool, configs: {pluginConfig}}); - const result = await App_.findEqualDiffs([refImagesInfo].concat(comparedImagesInfo)); - - assert.calledTwice(looksSame); - assert.isEmpty(result); - }); - - it('should compare each diff cluster', async () => { - const refImagesInfo = mkImagesInfo({ - diffClusters: [ - {left: 0, top: 0, right: 5, bottom: 5}, - {left: 10, top: 10, right: 15, bottom: 15} - ] - }); - const comparedImagesInfo = [mkImagesInfo({ - diffClusters: [ - {left: 0, top: 0, right: 5, bottom: 5}, - {left: 10, top: 10, right: 15, bottom: 15} - ] - })]; - - looksSame.yields(null, {equal: true}); - - const App_ = await mkApp_({tool, configs: {pluginConfig}}); - await App_.findEqualDiffs([refImagesInfo].concat(comparedImagesInfo)); - - assert.equal(looksSame.callCount, 4); - }); - - it('should return all found equal diffs', async () => { - looksSame.yields(null, {equal: true}); - const refImagesInfo = mkImagesInfo(); - const comparedImagesInfo = [mkImagesInfo(), mkImagesInfo()]; - const App_ = await mkApp_({tool, configs: {pluginConfig}}); + it('should find equal diffs for passed images', async () => { + const app = await mkApp_({tool}); - const result = await App_.findEqualDiffs([refImagesInfo].concat(comparedImagesInfo)); + await app.findEqualDiffs('images'); - assert.deepEqual(result, comparedImagesInfo); + assert.calledOnceWith(toolRunner.findEqualDiffs, 'images'); }); }); diff --git a/test/unit/lib/gui/tool-runner/index.js b/test/unit/lib/gui/tool-runner/index.js index 02bb22dc8..9e48e9246 100644 --- a/test/unit/lib/gui/tool-runner/index.js +++ b/test/unit/lib/gui/tool-runner/index.js @@ -7,15 +7,16 @@ const proxyquire = require('proxyquire'); const GuiReportBuilder = require('lib/report-builder/gui'); const constantFileNames = require('lib/constants/file-names'); const serverUtils = require('lib/server-utils'); -const {stubTool, stubConfig, mkTestResult, mkImagesInfo, mkState, mkSuite, mkSuiteTree} = require('test/unit/utils'); +const {stubTool, stubConfig, mkImagesInfo, mkState, mkSuite} = require('test/unit/utils'); -describe('lib/gui/tool-runner/hermione/index', () => { +describe('lib/gui/tool-runner/index', () => { const sandbox = sinon.createSandbox(); let reportBuilder; let ToolGuiReporter; let reportSubscriber; let hermione; let getDataFromDatabase; + let looksSame; const mkTestCollection_ = (testsTree = {}) => { return { @@ -54,6 +55,7 @@ describe('lib/gui/tool-runner/hermione/index', () => { reportBuilder = sinon.createStubInstance(GuiReportBuilder); reportSubscriber = sandbox.stub().named('reportSubscriber'); + looksSame = sandbox.stub().named('looksSame').yields(null, {equal: true}); sandbox.stub(GuiReportBuilder, 'create').returns(reportBuilder); reportBuilder.format.returns({prepareTestResult: sandbox.stub()}); @@ -62,6 +64,7 @@ describe('lib/gui/tool-runner/hermione/index', () => { getDataFromDatabase = sandbox.stub().returns({}); ToolGuiReporter = proxyquire(`lib/gui/tool-runner`, { + 'looks-same': looksSame, './report-subscriber': reportSubscriber, './utils': {findTestResult: sandbox.stub(), getDataFromDatabase}, '../../reporter-helpers': {updateReferenceImage: sandbox.stub().resolves()} @@ -200,17 +203,18 @@ describe('lib/gui/tool-runner/hermione/index', () => { const gui = initGuiReporter(hermione); await gui.initialize(); - const tests = [mkTestResult({ + const tests = [{ browserId: 'yabro', suite: {path: ['suite1']}, state: {}, + metaInfo: {}, imagesInfo: [mkImagesInfo({ stateName: 'plain1', actualImg: { size: {height: 100, width: 200} } })] - })]; + }]; await gui.updateReferenceImage(tests); @@ -233,10 +237,11 @@ describe('lib/gui/tool-runner/hermione/index', () => { const gui = initGuiReporter(hermione); await gui.initialize(); - const tests = [mkTestResult({ + const tests = [{ browserId: 'yabro', suite: {path: ['suite1']}, state: {}, + metaInfo: {}, imagesInfo: [ mkImagesInfo({ stateName: 'plain1', @@ -251,7 +256,7 @@ describe('lib/gui/tool-runner/hermione/index', () => { } }) ] - })]; + }]; await gui.updateReferenceImage(tests); @@ -268,6 +273,108 @@ describe('lib/gui/tool-runner/hermione/index', () => { }); }); + describe('findEqualDiffs', () => { + let compareOpts; + + beforeEach(() => { + hermione = stubTool(stubConfig({tolerance: 100500, antialiasingTolerance: 500100})); + hermione.readTests.resolves(mkTestCollection_()); + + compareOpts = { + tolerance: 100500, + antialiasingTolerance: 500100, + stopOnFirstFail: true, + shouldCluster: false + }; + + sandbox.stub(path, 'resolve'); + }); + + it('should stop comparison on first diff in reference images', async () => { + const gui = initGuiReporter(hermione, {configs: mkPluginConfig_({path: 'report_path'})}); + const refImagesInfo = mkImagesInfo({expectedImg: {path: 'ref-path-1'}}); + const comparedImagesInfo = [mkImagesInfo({expectedImg: {path: 'ref-path-2'}})]; + + path.resolve + .withArgs(process.cwd(), 'report_path', 'ref-path-1').returns('/ref-path-1') + .withArgs(process.cwd(), 'report_path', 'ref-path-2').returns('/ref-path-2'); + + looksSame.withArgs( + {source: '/ref-path-1', boundingBox: refImagesInfo.diffClusters[0]}, + {source: '/ref-path-2', boundingBox: comparedImagesInfo[0].diffClusters[0]}, + compareOpts + ).yields(null, {equal: false}); + + await gui.initialize(); + const result = await gui.findEqualDiffs([refImagesInfo, ...comparedImagesInfo]); + + assert.calledOnce(looksSame); + assert.isEmpty(result); + }); + + it('should stop comparison on diff in actual images', async () => { + const gui = initGuiReporter(hermione, {configs: mkPluginConfig_({path: 'report_path'})}); + const refImagesInfo = mkImagesInfo({actualImg: {path: 'act-path-1'}}); + const comparedImagesInfo = [mkImagesInfo({actualImg: {path: 'act-path-2'}})]; + + path.resolve + .withArgs(process.cwd(), 'report_path', 'act-path-1').returns('/act-path-1') + .withArgs(process.cwd(), 'report_path', 'act-path-2').returns('/act-path-2'); + + looksSame.onFirstCall().yields(null, {equal: true}); + looksSame.withArgs( + {source: '/act-path-1', boundingBox: refImagesInfo.diffClusters[0]}, + {source: '/act-path-2', boundingBox: comparedImagesInfo[0].diffClusters[0]}, + compareOpts + ).yields(null, {equal: false}); + + await gui.initialize(); + const result = await gui.findEqualDiffs([refImagesInfo, ...comparedImagesInfo]); + + assert.calledTwice(looksSame); + assert.isEmpty(result); + }); + + it('should compare each diff cluster', async () => { + const gui = initGuiReporter(hermione, {configs: mkPluginConfig_({path: 'report_path'})}); + const refImagesInfo = mkImagesInfo({ + diffClusters: [ + {left: 0, top: 0, right: 5, bottom: 5}, + {left: 10, top: 10, right: 15, bottom: 15} + ] + }); + const comparedImagesInfo = [mkImagesInfo({ + diffClusters: [ + {left: 0, top: 0, right: 5, bottom: 5}, + {left: 10, top: 10, right: 15, bottom: 15} + ] + })]; + + looksSame.yields(null, {equal: true}); + + await gui.initialize(); + await gui.findEqualDiffs([refImagesInfo, ...comparedImagesInfo]); + + assert.equal(looksSame.callCount, 4); + }); + + it('should return all found image ids with equal diffs', async () => { + const gui = initGuiReporter(hermione); + const refImagesInfo = {...mkImagesInfo(), id: 'selected-img-1'}; + const comparedImagesInfo = [ + {...mkImagesInfo(), id: 'compared-img-2'}, + {...mkImagesInfo(), id: 'compared-img-3'} + ]; + + looksSame.yields(null, {equal: true}); + + await gui.initialize(); + const result = await gui.findEqualDiffs([refImagesInfo, ...comparedImagesInfo]); + + assert.deepEqual(result, ['compared-img-2', 'compared-img-3']); + }); + }); + describe('finalize hermione', () => { it('should call reportBuilder.finalize', async () => { const gui = initGuiReporter(hermione); @@ -291,8 +398,7 @@ describe('lib/gui/tool-runner/hermione/index', () => { }); it('should log a warning that there is no data for reuse', async () => { - const suites = [mkSuiteTree()]; - reportBuilder.getResult.returns({suites}); + reportBuilder.getResult.returns({}); await gui.initialize(); @@ -327,12 +433,6 @@ describe('lib/gui/tool-runner/hermione/index', () => { assert.equal(gui.tree.baz, 'qux'); }); - it('"gui" flag', async () => { - await gui.initialize(); - - assert.isTrue(gui.tree.gui); - }); - it('"autoRun" from gui options', async () => { const guiOpts = {autoRun: true}; const configs = {...mkPluginConfig_(), ...mkToolCliOpts_({}, guiOpts)}; diff --git a/test/unit/lib/gui/tool-runner/report-subsciber.js b/test/unit/lib/gui/tool-runner/report-subsciber.js index fd4022049..8f1ce04a2 100644 --- a/test/unit/lib/gui/tool-runner/report-subsciber.js +++ b/test/unit/lib/gui/tool-runner/report-subsciber.js @@ -1,18 +1,16 @@ 'use strict'; const {EventEmitter} = require('events'); -const proxyquire = require('proxyquire'); +const reportSubscriber = require('lib/gui/tool-runner/report-subscriber'); const GuiReportBuilder = require('lib/report-builder/gui'); const clientEvents = require('lib/gui/constants/client-events'); const {RUNNING} = require('lib/constants/test-statuses'); -const utils = require('lib/gui/tool-runner/utils'); const {stubTool, stubConfig} = require('test/unit/utils'); describe('lib/gui/tool-runner/hermione/report-subscriber', () => { const sandbox = sinon.createSandbox(); let reportBuilder; let client; - let reportSubscriber; const events = { RUNNER_END: 'runnerEnd', @@ -34,14 +32,6 @@ describe('lib/gui/tool-runner/hermione/report-subscriber', () => { reportBuilder = sinon.createStubInstance(GuiReportBuilder); sandbox.stub(GuiReportBuilder, 'create').returns(reportBuilder); reportBuilder.format.returns(mkTestAdapterStub_()); - sandbox.stub(utils, 'findTestResult'); - - const findTestResult = sandbox.stub(); - reportSubscriber = proxyquire('lib/gui/tool-runner/report-subscriber', { - '../utils': { - findTestResult - } - }); client = new EventEmitter(); sandbox.spy(client, 'emit'); @@ -63,69 +53,92 @@ describe('lib/gui/tool-runner/hermione/report-subscriber', () => { describe('TEST_BEGIN', () => { it('should emit "BEGIN_STATE" event for client with correct data', () => { const hermione = mkHermione_(); - const testData = { - title: 'suite-title', - parent: {title: 'root-title', parent: {root: true}}, - browserId: 'bro' - }; + const testData = 'test-data'; + const formattedResult = mkTestAdapterStub_({id: 'some-id'}); - reportSubscriber(hermione, reportBuilder, client); + reportBuilder.format.withArgs(testData, RUNNING).returns(formattedResult); + reportBuilder.getTestBranch.withArgs('some-id').returns('test-tree-branch'); + reportSubscriber(hermione, reportBuilder, client); hermione.emit(hermione.events.TEST_BEGIN, testData); - assert.calledOnceWith(client.emit, clientEvents.BEGIN_STATE, { - browserId: 'bro', - suitePath: ['root-title', 'suite-title'], - status: RUNNING - }); + assert.calledOnceWith(client.emit, clientEvents.BEGIN_STATE, 'test-tree-branch'); }); }); - describe('TEST_RESULT', () => { + describe('TEST_PENDING', () => { it('should add skipped test result to report', async () => { const hermione = mkHermione_(); + const testData = 'test-data'; + const formattedResult = mkTestAdapterStub_(); + reportBuilder.format.withArgs(testData, hermione.events.TEST_PENDING).returns(formattedResult); reportSubscriber(hermione, reportBuilder, client); - reportBuilder.format.withArgs({foo: 'bar'}).returns(mkTestAdapterStub_({formatted: 'res'})); - hermione.emit(hermione.events.TEST_PENDING, {foo: 'bar'}); + hermione.emitAndWait(hermione.events.TEST_PENDING, testData); await hermione.emitAndWait(hermione.events.RUNNER_END); - assert.calledOnceWith(reportBuilder.addSkipped, sinon.match({formatted: 'res'})); + assert.calledOnceWith(reportBuilder.addSkipped, formattedResult); }); - it('should emit "TEST_RESULT" for client with test data', async () => { + it('should emit "TEST_RESULT" event for client with test data', async () => { const hermione = mkHermione_(); - utils.findTestResult.returns({name: 'foo'}); + const testData = 'test-data'; + const formattedResult = mkTestAdapterStub_({id: 'some-id'}); + + reportBuilder.format.withArgs(testData, hermione.events.TEST_PENDING).returns(formattedResult); + reportBuilder.getTestBranch.withArgs('some-id').returns('test-tree-branch'); reportSubscriber(hermione, reportBuilder, client); - await hermione.emitAndWait(hermione.events.TEST_PENDING, {}); + hermione.emitAndWait(hermione.events.TEST_PENDING, testData); + await hermione.emitAndWait(hermione.events.RUNNER_END); - assert.calledOnceWith(client.emit, clientEvents.TEST_RESULT, {name: 'foo'}); + assert.calledWith(client.emit, clientEvents.TEST_RESULT, 'test-tree-branch'); }); }); describe('TEST_FAIL', () => { it('should add correct attempt', async () => { const hermione = mkHermione_(); - reportBuilder.getCurrAttempt.returns(1); + const testData = 'test-data'; + const formattedResult = mkTestAdapterStub_(); - reportSubscriber(hermione, reportBuilder, client, ''); - hermione.emit(hermione.events.TEST_FAIL, {}); - await hermione.emitAndWait(hermione.events.RUNNER_END, {}); + reportBuilder.format.withArgs(testData, hermione.events.TEST_FAIL).returns(formattedResult); + reportBuilder.getCurrAttempt.withArgs(formattedResult).returns(1); + + reportSubscriber(hermione, reportBuilder, client); + hermione.emit(hermione.events.TEST_FAIL, testData); + await hermione.emitAndWait(hermione.events.RUNNER_END); assert.calledWithMatch(reportBuilder.addFail, {attempt: 1}); }); it('should save images before fail adding', async () => { const hermione = mkHermione_(); + const testData = 'test-data'; const formattedResult = mkTestAdapterStub_({saveTestImages: sandbox.stub()}); - reportBuilder.format.withArgs({some: 'res'}).returns(formattedResult); - reportSubscriber(hermione, reportBuilder, client, ''); - hermione.emit(hermione.events.TEST_FAIL, {some: 'res'}); + reportBuilder.format.withArgs(testData, hermione.events.TEST_FAIL).returns(formattedResult); + + reportSubscriber(hermione, reportBuilder, client); + hermione.emit(hermione.events.TEST_FAIL, testData); await hermione.emitAndWait(hermione.events.RUNNER_END); assert.callOrder(formattedResult.saveTestImages, reportBuilder.addFail); }); + + it('should emit "TEST_RESULT" event for client with test data', async () => { + const hermione = mkHermione_(); + const testData = 'test-data'; + const formattedResult = mkTestAdapterStub_({id: 'some-id'}); + + reportBuilder.format.withArgs(testData, hermione.events.TEST_FAIL).returns(formattedResult); + reportBuilder.getTestBranch.withArgs('some-id').returns('test-tree-branch'); + + reportSubscriber(hermione, reportBuilder, client); + hermione.emit(hermione.events.TEST_FAIL, testData); + await hermione.emitAndWait(hermione.events.RUNNER_END); + + assert.calledWith(client.emit, clientEvents.TEST_RESULT, 'test-tree-branch'); + }); }); }); diff --git a/test/unit/lib/gui/tool-runner/utils.js b/test/unit/lib/gui/tool-runner/utils.js deleted file mode 100644 index 33e99874d..000000000 --- a/test/unit/lib/gui/tool-runner/utils.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -const utils = require('lib/gui/tool-runner/utils'); -const {mkSuite, mkState, mkBrowserResult} = require('../../../utils'); - -describe('lib/gui/tool-runner/utils', () => { - describe('formatTests', () => { - it('should format suite with children and browsers', () => { - const suite = mkSuite({ - suitePath: ['suite1'], - children: [ - mkSuite({ - suitePath: ['suite1', 'suite2'], - children: [ - mkState({ - suitePath: ['suite1', 'suite2', 'state'], - browsers: [mkBrowserResult({name: 'chrome'})] - }) - ], - browsers: [mkBrowserResult({name: 'yabro'})] - }) - ] - }); - - const formattedTests = utils.formatTests(suite); - - assert.deepEqual(formattedTests, [ - { - 'browserId': 'yabro', - 'state': { - 'name': 'suite2' - }, - 'suite': { - 'path': [ - 'suite1' - ] - } - }, - { - 'browserId': 'chrome', - 'state': { - 'name': 'state' - }, - 'suite': { - 'path': [ - 'suite1', - 'suite2' - ] - } - } - ]); - }); - }); -}); diff --git a/test/unit/lib/report-builder/gui.js b/test/unit/lib/report-builder/gui.js index 0b0fb1f72..8e0b483fb 100644 --- a/test/unit/lib/report-builder/gui.js +++ b/test/unit/lib/report-builder/gui.js @@ -6,7 +6,7 @@ const serverUtils = require('lib/server-utils'); const TestAdapter = require('lib/test-adapter'); const GuiTestsTreeBuilder = require('lib/tests-tree-builder/gui'); const proxyquire = require('proxyquire'); -const {SUCCESS, FAIL, ERROR, SKIPPED, IDLE, UPDATED} = require('lib/constants/test-statuses'); +const {SUCCESS, FAIL, ERROR, SKIPPED, IDLE, RUNNING, UPDATED} = require('lib/constants/test-statuses'); const {mkFormattedTest} = require('../../utils'); describe('GuiReportBuilder', () => { @@ -73,8 +73,10 @@ describe('GuiReportBuilder', () => { sandbox.stub(GuiTestsTreeBuilder, 'create').returns(Object.create(GuiTestsTreeBuilder.prototype)); sandbox.stub(GuiTestsTreeBuilder.prototype, 'sortTree').returns({}); sandbox.stub(GuiTestsTreeBuilder.prototype, 'reuseTestsTree'); + sandbox.stub(GuiTestsTreeBuilder.prototype, 'getTestBranch').returns({}); + sandbox.stub(GuiTestsTreeBuilder.prototype, 'getTestsDataToUpdateRefs').returns([]); + sandbox.stub(GuiTestsTreeBuilder.prototype, 'getImageDataToFindEqualDiffs').returns({}); sandbox.stub(GuiTestsTreeBuilder.prototype, 'getImagesInfo').returns([]); - sandbox.stub(GuiTestsTreeBuilder.prototype, 'convertToOldFormat').returns({}); sandbox.stub(GuiTestsTreeBuilder.prototype, 'getLastResult').returns({}); sandbox.stub(GuiTestsTreeBuilder.prototype, 'addTestResult').returns({}); }); @@ -91,6 +93,16 @@ describe('GuiReportBuilder', () => { }); }); + describe('"addRunning" method', () => { + it(`should add "${RUNNING}" status to result`, async () => { + const reportBuilder = await mkGuiReportBuilder_(); + + reportBuilder.addRunning(stubTest_()); + + assert.equal(getTestResult_().status, RUNNING); + }); + }); + describe('"addSkipped" method', () => { it('should add skipped test to results', async () => { const reportBuilder = await mkGuiReportBuilder_(); @@ -203,11 +215,10 @@ describe('GuiReportBuilder', () => { GuiTestsTreeBuilder.prototype.getImagesInfo.returns(failedTest.imagesInfo); reportBuilder.addUpdated(updatedTest); - const {imagesInfo} = getTestResult_(); + const updatedTestResult = GuiTestsTreeBuilder.prototype.addTestResult.secondCall.args[0]; - assert.match(imagesInfo[0], {stateName: 'plain1', status: UPDATED}); - assert.match(imagesInfo[1], {stateName: 'plain2', status: FAIL}); - assert.equal(getTestResult_().status, FAIL); + assert.match(updatedTestResult.imagesInfo[0], {stateName: 'plain1', status: UPDATED}); + assert.match(updatedTestResult.imagesInfo[1], {stateName: 'plain2', status: FAIL}); }); it('should update last test image if state name was not passed', async () => { @@ -220,6 +231,7 @@ describe('GuiReportBuilder', () => { ] }); const updatedTest = stubTest_({ + id: 'result-2', imagesInfo: [ {status: UPDATED} ] @@ -229,7 +241,7 @@ describe('GuiReportBuilder', () => { GuiTestsTreeBuilder.prototype.getImagesInfo.returns(failedTest.imagesInfo); reportBuilder.addUpdated(updatedTest); - const {imagesInfo} = getTestResult_(); + const {imagesInfo} = GuiTestsTreeBuilder.prototype.addTestResult.secondCall.args[0]; assert.match(imagesInfo[0], {status: FAIL}); assert.match(imagesInfo[1], {status: UPDATED}); @@ -246,13 +258,32 @@ describe('GuiReportBuilder', () => { }); }); - describe('"reuseTestsTree" method', () => { - it('should call "reuseTestsTree" from tests tree builder', async () => { - const reportBuilder = await mkGuiReportBuilder_(); + [ + { + method: 'reuseTestsTree', + arg: 'some-tree' + }, + { + method: 'getTestBranch', + arg: 'test-id' + }, + { + method: 'getTestsDataToUpdateRefs', + arg: ['img-id-1', 'img-id-2'] + }, + { + method: 'getImageDataToFindEqualDiffs', + arg: 'img-id' + } + ].forEach(({method, arg}) => { + describe(`"${method}" method`, () => { + it(`should call "${method}" from tests tree builder`, async () => { + const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.reuseTestsTree('some-tree'); + reportBuilder[method](arg); - assert.calledOnceWith(GuiTestsTreeBuilder.prototype.reuseTestsTree, 'some-tree'); + assert.calledOnceWith(GuiTestsTreeBuilder.prototype[method], arg); + }); }); }); @@ -272,17 +303,6 @@ describe('GuiReportBuilder', () => { }); }); - describe('"getSuites" method', () => { - it('should return suites from tests tree builder', async () => { - const suites = {some: 'suite'}; - GuiTestsTreeBuilder.prototype.convertToOldFormat.returns({suites}); - - const reportBuilder = await mkGuiReportBuilder_(); - - assert.deepEqual(reportBuilder.getSuites(), suites); - }); - }); - describe('"getCurrAttempt" method', () => { [IDLE, SKIPPED].forEach((status) => { it(`should return attempt for last result if status is "${status}"`, async () => { diff --git a/test/unit/lib/server-utils.js b/test/unit/lib/server-utils.js index eacc54c3c..3fef2e44a 100644 --- a/test/unit/lib/server-utils.js +++ b/test/unit/lib/server-utils.js @@ -130,7 +130,7 @@ describe('server-utils', () => { }); describe('shouldUpdateAttempt', () => { - const IgnoreAttemptStatuses = ['SKIPPED', 'UPDATED', 'IDLE']; + const IgnoreAttemptStatuses = ['SKIPPED', 'UPDATED', 'RUNNING', 'IDLE']; IgnoreAttemptStatuses.forEach((s) => { const status = testStatuses[s]; diff --git a/test/unit/lib/static/components/controls/accept-opened-button.js b/test/unit/lib/static/components/controls/accept-opened-button.js new file mode 100644 index 000000000..757c822be --- /dev/null +++ b/test/unit/lib/static/components/controls/accept-opened-button.js @@ -0,0 +1,64 @@ +import React from 'react'; +import proxyquire from 'proxyquire'; +import {mkState, mkConnectedComponent} from '../utils'; + +describe('<AcceptOpenedButton />', () => { + const sandbox = sinon.sandbox.create(); + + let AcceptOpenedButton; + let actionsStub; + let selectors; + + beforeEach(() => { + actionsStub = {acceptOpened: sandbox.stub().returns({type: 'some-type'})}; + selectors = {getAcceptableOpenedImageIds: sandbox.stub().returns([])}; + + AcceptOpenedButton = proxyquire('lib/static/components/controls/accept-opened-button', { + '../../modules/actions': actionsStub, + '../../modules/selectors/tree': selectors + }).default; + }); + + afterEach(() => sandbox.restore()); + + it('should be disabled if acceptable opened images are not present', () => { + const state = mkState({initialState: {processing: false}}); + const acceptableOpenedImageIds = []; + selectors.getAcceptableOpenedImageIds.withArgs(state).returns(acceptableOpenedImageIds); + + const component = mkConnectedComponent(<AcceptOpenedButton />, state); + + assert.isTrue(component.find('[label="Accept opened"]').prop('isDisabled')); + }); + + it('should be disabled while processing something', () => { + const state = mkState({initialState: {processing: true}}); + const acceptableOpenedImageIds = ['img-id-1']; + selectors.getAcceptableOpenedImageIds.withArgs(state).returns(acceptableOpenedImageIds); + + const component = mkConnectedComponent(<AcceptOpenedButton />, state); + + assert.isTrue(component.find('[label="Accept opened"]').prop('isDisabled')); + }); + + it('should be enabled if acceptable opened images are present', () => { + const state = mkState({initialState: {processing: false}}); + const acceptableOpenedImageIds = ['img-id-1']; + selectors.getAcceptableOpenedImageIds.withArgs(state).returns(acceptableOpenedImageIds); + + const component = mkConnectedComponent(<AcceptOpenedButton />, state); + + assert.isFalse(component.find('[label="Accept opened"]').prop('isDisabled')); + }); + + it('should call "acceptOpened" action on click', () => { + const state = mkState({initialState: {processing: false}}); + const acceptableOpenedImageIds = ['img-id-1']; + selectors.getAcceptableOpenedImageIds.withArgs(state).returns(acceptableOpenedImageIds); + + const component = mkConnectedComponent(<AcceptOpenedButton />, state); + component.find('[label="Accept opened"]').simulate('click'); + + assert.calledOnceWith(actionsStub.acceptOpened, acceptableOpenedImageIds); + }); +}); diff --git a/test/unit/lib/static/components/controls/find-same-diffs-button.js b/test/unit/lib/static/components/controls/find-same-diffs-button.js new file mode 100644 index 000000000..c8ae923d5 --- /dev/null +++ b/test/unit/lib/static/components/controls/find-same-diffs-button.js @@ -0,0 +1,83 @@ +import React from 'react'; +import proxyquire from 'proxyquire'; +import {defaults} from 'lodash'; +import {mkState, mkConnectedComponent} from '../utils'; + +describe('<FindSameDiffsButton />', () => { + const sandbox = sinon.sandbox.create(); + + let FindSameDiffsButton; + let actionsStub; + let selectors; + + const mkFindSameDiffsButton = (props = {}, initialState = {}) => { + props = defaults(props, { + imageId: 'default-img-id', + browserId: 'default-browser-id', + isDisabled: false + }); + + initialState = defaults(initialState, { + tree: { + browsers: { + byId: { + 'default-browser-id': { + name: 'yabro' + } + } + } + } + }); + + return mkConnectedComponent(<FindSameDiffsButton {...props} />, {initialState}); + }; + + beforeEach(() => { + actionsStub = {findSameDiffs: sandbox.stub().returns({type: 'some-type'})}; + selectors = {getFailedOpenedImageIds: sandbox.stub().returns([])}; + + FindSameDiffsButton = proxyquire('lib/static/components/controls/find-same-diffs-button', { + '../../modules/actions': actionsStub, + '../../modules/selectors/tree': selectors + }).default; + }); + + afterEach(() => sandbox.restore()); + + it('should be disabled if passed prop "isDisabled" is true', () => { + const component = mkFindSameDiffsButton({isDisabled: true}); + + assert.isTrue(component.find('[label="🔍 Find same diffs"]').prop('isDisabled')); + }); + + it('should be enabled if passed prop "isDisabled" is false', () => { + const component = mkFindSameDiffsButton({isDisabled: false}); + + assert.isFalse(component.find('[label="🔍 Find same diffs"]').prop('isDisabled')); + }); + + it('should call "findSameDiffs" action on click', () => { + const initialState = { + tree: { + browsers: { + byId: { + 'browser-id': { + name: 'yabro' + } + } + } + } + }; + const state = mkState({initialState}); + const failedOpenedImageIds = ['img-1', 'img-2']; + selectors.getFailedOpenedImageIds.withArgs(state).returns(failedOpenedImageIds); + + const component = mkConnectedComponent( + <FindSameDiffsButton imageId="img-1" browserId="browser-id" isDisabled={false} />, + {state} + ); + component.find('[label="🔍 Find same diffs"]').simulate('click'); + + assert.calledOnceWith(actionsStub.findSameDiffs, 'img-1', failedOpenedImageIds, 'yabro'); + }); +}); diff --git a/test/unit/lib/static/components/controls/gui-controls.js b/test/unit/lib/static/components/controls/gui-controls.js index fbb11a9f4..c91946d39 100644 --- a/test/unit/lib/static/components/controls/gui-controls.js +++ b/test/unit/lib/static/components/controls/gui-controls.js @@ -1,23 +1,27 @@ import React from 'react'; import RunButton from 'lib/static/components/controls/run-button'; +import AcceptOpenedButton from 'lib/static/components/controls/accept-opened-button'; import proxyquire from 'proxyquire'; -import {mkConnectedComponent} from '../utils'; +import {mkState, mkConnectedComponent} from '../utils'; -describe('<ControlButtons />', () => { +describe('<GuiControls />', () => { const sandbox = sinon.sandbox.create(); - let ControlButtons; - let actionsStub; + let GuiControls, actionsStub, selectors; beforeEach(() => { actionsStub = { runAllTests: sandbox.stub().returns({type: 'some-type'}), - runFailedTests: sandbox.stub().returns({type: 'some-type'}), - acceptOpened: sandbox.stub().returns({type: 'some-type'}) + runFailedTests: sandbox.stub().returns({type: 'some-type'}) }; - ControlButtons = proxyquire('lib/static/components/controls/gui-controls', { - '../../modules/actions': actionsStub + selectors = { + getFailedTests: sandbox.stub().returns([]) + }; + + GuiControls = proxyquire('lib/static/components/controls/gui-controls', { + '../../modules/actions': actionsStub, + '../../modules/selectors/tree': selectors }).default; }); @@ -25,31 +29,31 @@ describe('<ControlButtons />', () => { describe('"Run" button', () => { it('should be disabled if no suites to run', () => { - const component = mkConnectedComponent(<ControlButtons />, { - initialState: {suiteIds: {all: []}, running: false} + const component = mkConnectedComponent(<GuiControls />, { + initialState: {tree: {suites: {allRootIds: []}}, processing: false} }); assert.isTrue(component.find(RunButton).prop('isDisabled')); }); it('should be enabled if suites exist to run', () => { - const component = mkConnectedComponent(<ControlButtons />, { - initialState: {suiteIds: {all: ['some-suite']}, running: false} + const component = mkConnectedComponent(<GuiControls />, { + initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} }); assert.isFalse(component.find(RunButton).prop('isDisabled')); }); it('should be disabled while processing something', () => { - const component = mkConnectedComponent(<ControlButtons />, { - initialState: {processing: true} + const component = mkConnectedComponent(<GuiControls />, { + initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: true} }); assert.isTrue(component.find(RunButton).prop('isDisabled')); }); it('should pass "autoRun" prop', () => { - const component = mkConnectedComponent(<ControlButtons />, { + const component = mkConnectedComponent(<GuiControls />, { initialState: {autoRun: true} }); @@ -57,8 +61,8 @@ describe('<ControlButtons />', () => { }); it('should call "runAllTests" action on click', () => { - const component = mkConnectedComponent(<ControlButtons />, { - initialState: {suiteIds: {all: ['some-suite']}, running: false} + const component = mkConnectedComponent(<GuiControls />, { + initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} }); component.find(RunButton).simulate('click'); @@ -67,61 +71,48 @@ describe('<ControlButtons />', () => { }); }); - [ - {name: 'Retry failed tests', handler: 'runFailedTests'}, - {name: 'Accept opened', handler: 'acceptOpened'} - ].forEach((button) => { - describe(`"${button.name}" button`, () => { - it('should be disabled if no failed suites to run', () => { - const component = mkConnectedComponent(<ControlButtons />, { - initialState: {suiteIds: {all: [], failed: []}, running: false} - }); - - assert.isTrue(component.find(`[label="${button.name}"]`).prop('isDisabled')); + describe('"Retry failed tests" button', () => { + it('should be disabled if no failed suites to run', () => { + const component = mkConnectedComponent(<GuiControls />, { + initialState: {tree: {suites: {failedRootIds: []}}, processing: false} }); - it('should be enabled if failed suites exist to run', () => { - const component = mkConnectedComponent(<ControlButtons />, { - initialState: { - suites: {suite1: {}}, - suiteIds: {all: [], failed: ['suite1']}, - running: false - } - }); + assert.isTrue(component.find('[label="Retry failed tests"]').prop('isDisabled')); + }); - assert.isFalse(component.find(`[label="${button.name}"]`).prop('isDisabled')); + it('should be enabled if failed suites exist to run', () => { + const component = mkConnectedComponent(<GuiControls />, { + initialState: {tree: {suites: {failedRootIds: ['suite']}}, processing: false} }); - it('should be disabled while tests running', () => { - const component = mkConnectedComponent(<ControlButtons />, { - initialState: {running: true} - }); + assert.isFalse(component.find('[label="Retry failed tests"]').prop('isDisabled')); + }); - assert.isTrue(component.find(`[label="${button.name}"]`).prop('isDisabled')); + it('should be disabled while processing something', () => { + const component = mkConnectedComponent(<GuiControls />, { + initialState: {tree: {suites: {failedRootIds: ['suite']}}, processing: true} }); - it('should be disabled while processing something', () => { - const component = mkConnectedComponent(<ControlButtons />, { - initialState: {processing: true} - }); + assert.isTrue(component.find('[label="Retry failed tests"]').prop('isDisabled')); + }); - assert.isTrue(component.find(`[label="${button.name}"]`).prop('isDisabled')); - }); + it('should call "runFailedTests" action on click', () => { + const failedTests = [{testName: 'suite test', browserName: 'yabro'}]; + const state = mkState({initialState: {tree: {suites: {failedRootIds: ['suite']}}, processing: false}}); + selectors.getFailedTests.withArgs(state).returns(failedTests); + const component = mkConnectedComponent(<GuiControls />, {state}); - it(`should call "${button.handler}" action on click`, () => { - const failedSuite = {name: 'suite1', status: 'fail'}; - const component = mkConnectedComponent(<ControlButtons />, { - initialState: { - suites: {suite1: failedSuite}, - suiteIds: {all: [], failed: ['suite1']}, - running: false - } - }); + component.find('[label="Retry failed tests"]').simulate('click'); - component.find(`[label="${button.name}"]`).simulate('click'); + assert.calledOnceWith(actionsStub.runFailedTests, failedTests); + }); + }); - assert.calledOnceWith(actionsStub[button.handler], [failedSuite]); - }); + describe('"Accept opened" button', () => { + it('should render button', () => { + const component = mkConnectedComponent(<GuiControls />); + + assert.isTrue(component.contains(AcceptOpenedButton)); }); }); }); diff --git a/test/unit/lib/static/components/section/body/index.js b/test/unit/lib/static/components/section/body/index.js index 2ee9353e9..563bfba4d 100644 --- a/test/unit/lib/static/components/section/body/index.js +++ b/test/unit/lib/static/components/section/body/index.js @@ -1,312 +1,119 @@ import React from 'react'; import proxyquire from 'proxyquire'; import {defaults} from 'lodash'; -import MetaInfo from 'lib/static/components/section/body/meta-info'; -import {mkConnectedComponent, mkTestResult_, mkSuite_, mkImg_} from '../../utils'; -import {mkBrowserResult} from '../../../../../utils'; -import {SUCCESS, FAIL, ERROR} from 'lib/constants/test-statuses'; +import {mkConnectedComponent} from '../../utils'; describe('<Body />', () => { const sandbox = sinon.sandbox.create(); + let Body, Result, RetrySwitcher, actionsStub; - let Body; - let actionsStub; - let utilsStub; - - const mkBodyComponent = (bodyProps = {}, initialState = {}) => { - const browser = bodyProps.browser || mkBrowserResult(); - - bodyProps = defaults(bodyProps, { - result: mkTestResult_(), - retries: [], - suite: mkSuite_(), - browser - }); - - actionsStub.changeTestRetry.callsFake(({retryIndex}) => { - browser.retryIndex = retryIndex; - return {type: 'some-type'}; + const mkBodyComponent = (props = {}, initialState = {}) => { + props = defaults(props, { + browserId: 'suite test yabro', + browserName: 'yabro', + testName: 'suite test', + resultIds: ['result-1'] }); - return mkConnectedComponent(<Body {...bodyProps} />, {initialState}); + return mkConnectedComponent(<Body {...props} />, {initialState}); }; beforeEach(() => { actionsStub = { - acceptTest: sandbox.stub().returns({type: 'some-type'}), retryTest: sandbox.stub().returns({type: 'some-type'}), - toggleTestResult: sandbox.stub().returns({type: 'some-type'}), - toggleStateResult: sandbox.stub().returns({type: 'some-type'}), - changeTestRetry: sandbox.stub().returns({type: 'some-type'}), - findSameDiffs: sandbox.stub().returns({type: 'some-type'}) + changeTestRetry: sandbox.stub().returns({type: 'some-type'}) }; - utilsStub = {isAcceptable: sandbox.stub()}; - - const State = proxyquire('lib/static/components/state', { - '../../modules/utils': utilsStub - }); + Result = sinon.stub().returns(null); + RetrySwitcher = sinon.stub().returns(null); Body = proxyquire('lib/static/components/section/body', { '../../../modules/actions': actionsStub, - '../../state': State + './result': {default: Result}, + './retry-switcher': {default: RetrySwitcher} }).default; }); afterEach(() => sandbox.restore()); - it('should render retry button if "gui" is running', () => { - const component = mkBodyComponent({}, {gui: true}); - - assert.equal(component.find('.button_type_suite-controls').first().text(), '↻ Retry'); - }); - - it('should not render retry button if "gui" is not running', () => { - const component = mkBodyComponent({}, {gui: false}); - - assert.lengthOf(component.find('.button_type_suite-controls'), 0); - }); - - it('should render state for each state image', () => { - const imagesInfo = [ - {stateName: 'plain1', status: ERROR, actualImg: mkImg_(), error: {}}, - {stateName: 'plain2', status: ERROR, actualImg: mkImg_(), error: {}} - ]; - const testResult = mkTestResult_({name: 'bro', imagesInfo}); - - const component = mkBodyComponent({result: testResult}); - - assert.lengthOf(component.find('.tab'), 2); - }); - - it('should not render state if state images does not exist and test passed successfully', () => { - const testResult = mkTestResult_({status: SUCCESS}); - - const component = mkBodyComponent({result: testResult}); - - assert.lengthOf(component.find('.tab'), 0); - }); - - it('should render additional tab if test errored without screenshot', () => { - const imagesInfo = [{stateName: 'plain1', status: SUCCESS, expectedImg: mkImg_()}]; - const testResult = mkTestResult_({status: ERROR, multipleTabs: true, error: {}, imagesInfo}); - - const component = mkBodyComponent({result: testResult}); - - assert.lengthOf(component.find('.tab'), 2); - }); - - it('should render tab with error item if test errored without images', () => { - const testResult = mkTestResult_({status: ERROR, error: {foo: 'bar'}, imagesInfo: []}); - - const component = mkBodyComponent({result: testResult}); - - assert.lengthOf(component.find('.error__item'), 1); - }); - - describe('"acceptTest" action', () => { - it('should call on "Accept" button click', () => { - const imagesInfo = [{stateName: 'plain', status: ERROR, actualImg: mkImg_(), error: {}, image: true, opened: true}]; - const testResult = mkTestResult_({name: 'bro', imagesInfo}); - const suite = mkSuite_({name: 'some-suite'}); - utilsStub.isAcceptable.withArgs(imagesInfo[0]).returns(true); - - const component = mkBodyComponent({result: testResult, suite}, {view: {expand: 'all'}}); - component.find('[label="✔ Accept"]').simulate('click'); - - assert.calledOnceWith(actionsStub.acceptTest, suite, 'bro', 'plain'); - }); - }); - - describe('"findSameDiffs" action', () => { - it('should call on "Find same diffs" button click', () => { - const imagesInfo = [{stateName: 'plain', status: FAIL, actualImg: mkImg_(), error: {}, image: true, opened: true}]; - const testResult = mkTestResult_({name: 'bro', imagesInfo}); - const browser = mkBrowserResult(); - const suite = mkSuite_({name: 'some-suite', suitePath: ['some-suite']}); - const initialState = {view: {expand: 'all'}, suiteIds: {failed: ['some-suite']}, suites: {[suite.name]: suite}}; - - const component = mkBodyComponent({result: testResult, browser, suite}, initialState); - component.find('[label="🔍 Find same diffs"]').simulate('click'); - - assert.calledOnceWith(actionsStub.findSameDiffs, { - suitePath: suite.suitePath, browser, stateName: 'plain', fails: [suite] - }); - }); - }); - - describe('"toggleTestResult" action', () => { - it('should call on mount', () => { - const testResult = mkTestResult_({name: 'bro'}); - const suite = mkSuite_({suitePath: ['some-suite']}); - - mkBodyComponent({result: testResult, suite}); - - assert.calledOnceWith(actionsStub.toggleTestResult, { - browserId: 'bro', suitePath: ['some-suite'], opened: true - }); - }); - - it('should call on unmount', () => { - const testResult = mkTestResult_({name: 'bro'}); - const suite = mkSuite_({suitePath: ['some-suite']}); - - const component = mkBodyComponent({result: testResult, suite}); - component.unmount(); + describe('"Retry" button', () => { + it('should render if "gui" is running', () => { + const component = mkBodyComponent({}, {gui: true}); - assert.calledTwice(actionsStub.toggleTestResult); - assert.calledWith(actionsStub.toggleTestResult.secondCall, { - browserId: 'bro', suitePath: ['some-suite'], opened: false - }); + assert.equal(component.find('.button_type_suite-controls').first().text(), '↻ Retry'); }); - }); - - describe('"toggleStateResult" action', () => { - it('should call on click to state', () => { - const imagesInfo = [{stateName: 'plain', status: SUCCESS, opened: false, expectedImg: mkImg_()}]; - const testResult = mkTestResult_({name: 'bro', imagesInfo}); - const suite = mkSuite_({name: 'some-suite', suitePath: ['some-suite']}); - const component = mkBodyComponent({result: testResult, suite}, {view: {expand: 'errors'}}); - component.find('.state-title').simulate('click'); + it('should not render if "gui" is not running', () => { + const component = mkBodyComponent({}, {gui: false}); - assert.calledWith( - actionsStub.toggleStateResult, - {stateName: 'plain', browserId: 'bro', suitePath: ['some-suite'], retryIndex: 0, opened: true} - ); + assert.lengthOf(component.find('.button_type_suite-controls'), 0); }); - }); - describe('"changeTestRetry" action', () => { - it('should call on mount', () => { - const testResult = mkTestResult_({name: 'bro'}); - const suite = mkSuite_({suitePath: ['some-suite']}); - - mkBodyComponent({result: testResult, suite, retries: [mkTestResult_()]}); + it('should be disabled while tests running', () => { + const component = mkBodyComponent({}, {running: true}); - assert.calledOnceWith(actionsStub.changeTestRetry, {browserId: 'bro', suitePath: ['some-suite'], retryIndex: 1}); + assert.isTrue(component.find('[label="↻ Retry"]').prop('isDisabled')); }); - it('should call action on click in switcher retry button', () => { - const testResult = mkTestResult_({name: 'bro'}); - const suite = mkSuite_({suitePath: ['some-suite']}); - - const component = mkBodyComponent({result: testResult, retries: [mkTestResult_()], suite}); - component.find('.tab-switcher__button:first-child').simulate('click'); + it('should be enabled if tests are not started yet', () => { + const component = mkBodyComponent({}, {running: false}); - assert.calledTwice(actionsStub.changeTestRetry); - assert.calledWith( - actionsStub.changeTestRetry.firstCall, - {browserId: 'bro', suitePath: ['some-suite'], retryIndex: 1} - ); - assert.calledWith( - actionsStub.changeTestRetry.secondCall, - {browserId: 'bro', suitePath: ['some-suite'], retryIndex: 0} - ); + assert.isFalse(component.find('[label="↻ Retry"]').prop('isDisabled')); }); - it('should change active result on click in switcher retry button', () => { - const suite = mkSuite_({suitePath: ['some-suite']}); - - const component = mkBodyComponent({ - result: mkTestResult_({suiteUrl: 'foo'}), - retries: [mkTestResult_({suiteUrl: 'bar'})], - suite - }); + it('should call action "retryTest" on "handler" prop calling', () => { + const testName = 'suite test'; + const browserName = 'yabro'; + const component = mkBodyComponent({testName, browserName}, {running: false}); - component.find('.tab-switcher__button:first-child').simulate('click'); + component.find('[label="↻ Retry"]').simulate('click'); - const metaInfoProps = component.find(MetaInfo).props(); - assert.equal(metaInfoProps.suiteUrl, 'bar'); + assert.calledOnceWith(actionsStub.retryTest, {testName, browserName}); }); }); - describe('errored additional tab', () => { - it('should render if test errored without screenshot and tool can use multi tabs', () => { - const imagesInfo = [{stateName: 'plain1', status: SUCCESS, expectedImg: mkImg_()}]; - const testResult = mkTestResult_({status: ERROR, multipleTabs: true, error: {}, imagesInfo}); - - const component = mkBodyComponent({result: testResult}); - - assert.lengthOf(component.find('.tab'), 2); - }); - - it('should not render if tool does not use multi tabs', () => { - const imagesInfo = [{stateName: 'plain1', status: SUCCESS, expectedImg: mkImg_()}]; - const testResult = mkTestResult_({status: ERROR, multipleTabs: false, error: {}, screenshot: 'some-screen', imagesInfo}); - - const component = mkBodyComponent({result: testResult}); + describe('"RetrySwitcher" component', () => { + it('should not render if test has only one result', () => { + mkBodyComponent({resultIds: ['result-1']}); - assert.lengthOf(component.find('.tab'), 1); + assert.notCalled(RetrySwitcher); }); - it('should not render if test errored with screenshot', () => { - const imagesInfo = [{stateName: 'plain1', status: SUCCESS, expectedImg: mkImg_()}]; - const testResult = mkTestResult_({status: ERROR, multipleTabs: true, error: {}, screenshot: 'some-screen', imagesInfo}); + it('should render if test has more than one result', () => { + const resultIds = ['result-1', 'result-2']; - const component = mkBodyComponent({result: testResult}); + mkBodyComponent({resultIds}); - assert.lengthOf(component.find('.tab'), 1); + assert.calledOnceWith(RetrySwitcher, {resultIds, retryIndex: 1, onChange: sinon.match.func}); }); - [SUCCESS, FAIL].forEach((status) => { - it(`should not render if test ${status}ed`, () => { - const imagesInfo = [{stateName: 'plain1', status: SUCCESS, expectedImg: mkImg_()}]; - const testResult = mkTestResult_({status, multipleTabs: true, error: {}, imagesInfo}); + it('should call "changeTestRetry" action on call "onChange" prop', () => { + const browserId = 'yabro'; + const resultIds = ['result-1', 'result-2']; - const component = mkBodyComponent({result: testResult}); + mkBodyComponent({browserId, resultIds}); + RetrySwitcher.firstCall.args[0].onChange(0); - assert.lengthOf(component.find('.tab'), 1); - }); + assert.calledWith(actionsStub.changeTestRetry.lastCall, {browserId, retryIndex: 0}); }); }); - describe('"Retry" button', () => { - it('should be disabled while tests running', () => { - const component = mkBodyComponent({}, {running: true}); - - assert.isTrue(component.find('[label="↻ Retry"]').prop('isDisabled')); - }); - - it('should be enabled if tests are not started yet', () => { - const component = mkBodyComponent({}, {running: false}); - - assert.isFalse(component.find('[label="↻ Retry"]').prop('isDisabled')); - }); - - it('should call action "retryTest" on "handler" prop calling', () => { - const testResult = mkTestResult_({name: 'bro'}); - const suite = mkSuite_(); + describe('"Result" component', () => { + it('should render with current active result id and test name', () => { + const testName = 'suite test'; + const resultIds = ['result-1', 'result-2']; - const component = mkBodyComponent({result: testResult, suite}, {running: false}); - component.find('[label="↻ Retry"]').simulate('click'); + mkBodyComponent({testName, resultIds}); - assert.calledOnceWith(actionsStub.retryTest, suite, 'bro'); + assert.calledOnceWith(Result, {resultId: 'result-2', testName}); }); }); - describe('meta-info', () => { - it('should pass "getExtraMetaInfo" props to meta info component', () => { - const component = mkBodyComponent(); - const metaInfoProps = component.find(MetaInfo).props(); - - assert.isFunction(metaInfoProps.getExtraMetaInfo); - }); - - it('should return result of calling stringified function from "metaInfoExtenders"', () => { - const suite = mkSuite_(); - const extraItems = {item1: 1}; - const extender = () => 'foo-bar-baz'; - const metaInfoExtenders = {extender1: extender.toString()}; - - const component = mkBodyComponent({suite}, { - apiValues: {extraItems, metaInfoExtenders} - }); - - const metaInfoProps = component.find(MetaInfo).props(); - const result = metaInfoProps.getExtraMetaInfo(); + describe('"changeTestRetry" action', () => { + it('should call on mount', () => { + mkBodyComponent({browserId: 'some-id', resultIds: ['result-1', 'result-2']}); - assert.deepEqual(result, {extender1: 'foo-bar-baz'}); + assert.calledOnceWith(actionsStub.changeTestRetry, {browserId: 'some-id', retryIndex: 1}); }); }); }); diff --git a/test/unit/lib/static/components/section/body/meta-info.js b/test/unit/lib/static/components/section/body/meta-info.js index 82bb09ad1..a30133407 100644 --- a/test/unit/lib/static/components/section/body/meta-info.js +++ b/test/unit/lib/static/components/section/body/meta-info.js @@ -1,34 +1,33 @@ import React from 'react'; -import {defaults} from 'lodash'; +import {defaultsDeep} from 'lodash'; import MetaInfo from 'lib/static/components/section/body/meta-info'; import {mkConnectedComponent} from '../../utils'; describe('<MetaInfo />', () => { - const sandbox = sinon.createSandbox(); - - let getExtraMetaInfo; - const mkMetaInfoComponent = (props = {}, initialState = {}) => { - props = defaults(props, { - metaInfo: {}, - suiteUrl: 'default-url', - getExtraMetaInfo + props = defaultsDeep(props, { + result: { + metaInfo: {}, + suiteUrl: 'default-url' + }, + testName: 'default suite test' }); return mkConnectedComponent(<MetaInfo {...props} />, {initialState}); }; - beforeEach(() => { - getExtraMetaInfo = sandbox.stub().named('getExtraMetaInfo').returns({}); - }); - - afterEach(() => sandbox.restore()); - - it('should render all meta info from test, extra meta info and link to url', () => { - getExtraMetaInfo.returns({baz: 'qux'}); - const expectedMetaInfo = ['foo: bar', 'baz: qux', 'url: some-url']; + it('should render meta info from result, extra meta and link to url', () => { + const result = {metaInfo: {foo: 'bar'}, suiteUrl: 'some-url'}; + const testName = 'suite test'; + const apiValues = { + extraItems: {baz: 'qux'}, + metaInfoExtenders: { + baz: ((data, extraItems) => `${data.testName}_${extraItems.baz}`).toString() + } + }; + const expectedMetaInfo = ['foo: bar', `baz: ${testName}_qux`, 'url: some-url']; - const component = mkMetaInfoComponent({metaInfo: {foo: 'bar'}, suiteUrl: 'some-url'}); + const component = mkMetaInfoComponent({result, testName}, {apiValues}); component.find('.meta-info__item').forEach((node, i) => { assert.equal(node.text(), expectedMetaInfo[i]); @@ -36,18 +35,20 @@ describe('<MetaInfo />', () => { }); it('should render meta-info with non-primitive values', () => { + const result = { + metaInfo: { + foo1: {bar: 'baz'}, + foo2: [{bar: 'baz'}] + }, + suiteUrl: 'some-url' + }; const expectedMetaInfo = [ 'foo1: {"bar":"baz"}', 'foo2: [{"bar":"baz"}]', 'url: some-url' ]; - const metaInfo = { - foo1: {bar: 'baz'}, - foo2: [{bar: 'baz'}] - }; - - const component = mkMetaInfoComponent({metaInfo, suiteUrl: 'some-url'}); + const component = mkMetaInfoComponent({result}); component.find('.details_type_text_item').forEach((node, i) => { assert.equal(node.text(), expectedMetaInfo[i]); @@ -67,9 +68,12 @@ describe('<MetaInfo />', () => { } ].forEach((stub) => { it(`should render link in meta info based upon metaInfoBaseUrls ${stub.type}`, () => { + const result = { + metaInfo: {file: 'test/file'} + }; const initialConfig = {config: {metaInfoBaseUrls: stub.metaInfoBaseUrls}}; - const component = mkMetaInfoComponent({metaInfo: {file: 'test/file'}}, initialConfig); + const component = mkMetaInfoComponent({result}, initialConfig); assert.equal(component.find('.meta-info__item:first-child').text(), 'file: test/file'); assert.equal(component.find('.meta-info__item:first-child a').prop('href'), stub.expectedFileUrl); diff --git a/test/unit/lib/static/components/section/body/result.js b/test/unit/lib/static/components/section/body/result.js new file mode 100644 index 000000000..2a9513905 --- /dev/null +++ b/test/unit/lib/static/components/section/body/result.js @@ -0,0 +1,120 @@ +import React from 'react'; +import proxyquire from 'proxyquire'; +import {defaults} from 'lodash'; +import {FAIL, SUCCESS} from 'lib/constants/test-statuses'; +import {mkConnectedComponent} from '../../utils'; + +describe('<Result />', () => { + const sandbox = sinon.sandbox.create(); + let Result, MetaInfo, Description, Tabs; + + const mkResult = (props = {}, initialState = {}) => { + props = defaults(props, { + resultId: 'default-id', + testName: 'suite test' + }); + + initialState = defaults(initialState, { + tree: { + results: { + byId: { + 'default-id': { + status: SUCCESS, + imageIds: [] + } + } + } + } + }); + + return mkConnectedComponent(<Result {...props} />, {initialState}); + }; + + beforeEach(() => { + MetaInfo = sinon.stub().returns(null); + Description = sinon.stub().returns(null); + Tabs = sinon.stub().returns(null); + + Result = proxyquire('lib/static/components/section/body/result', { + './meta-info': {default: MetaInfo}, + './description': {default: Description}, + './tabs': {default: Tabs} + }).default; + }); + + afterEach(() => sandbox.restore()); + + 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 + } + } + } + }; + + mkResult({resultId: 'result-1', testName: 'test-name'}, initialState); + + assert.calledOnceWith(MetaInfo, {result, testName: 'test-name'}); + }); + }); + + describe('"Description" component', () => { + it('should not render if description does not exists in result', () => { + const result = {status: FAIL, imageIds: [], description: null}; + const initialState = { + tree: { + results: { + byId: { + 'result-1': result + } + } + } + }; + + mkResult({resultId: 'result-1'}, initialState); + + assert.notCalled(Description); + }); + + it('should render if description exists in result', () => { + const result = {status: FAIL, imageIds: [], description: 'some-descr'}; + const initialState = { + tree: { + results: { + byId: { + 'result-1': result + } + } + } + }; + + mkResult({resultId: 'result-1'}, initialState); + + assert.calledOnceWith(Description, {content: 'some-descr'}); + }); + }); + + describe('"Tabs" component', () => { + it('should render with result prop', () => { + const result = {status: FAIL, imageIds: []}; + const initialState = { + tree: { + results: { + byId: { + 'result-1': result + } + } + } + }; + + mkResult({resultId: 'result-1'}, initialState); + + assert.calledOnceWith(Tabs, {result}); + }); + }); +}); diff --git a/test/unit/lib/static/components/section/body/retry-switcher/index.js b/test/unit/lib/static/components/section/body/retry-switcher/index.js new file mode 100644 index 000000000..63423feca --- /dev/null +++ b/test/unit/lib/static/components/section/body/retry-switcher/index.js @@ -0,0 +1,57 @@ +import React from 'react'; +import proxyquire from 'proxyquire'; +import {defaults} from 'lodash'; + +describe('<RetrySwitcher >', () => { + const sandbox = sinon.sandbox.create(); + let RetrySwitcher, RetrySwitcherItem; + + const mkRetrySwitcher = (props = {}) => { + props = defaults(props, { + resultIds: ['default-id'], + retryIndex: 0, + onChange: sinon.stub() + }); + + return mount(<RetrySwitcher {...props} />); + }; + + beforeEach(() => { + RetrySwitcherItem = sinon.stub().returns(null); + + RetrySwitcher = proxyquire('lib/static/components/section/body/retry-switcher', { + './item': {default: RetrySwitcherItem} + }).default; + }); + + afterEach(() => sandbox.restore()); + + it('should not render any tab switcher button if test did not retry', () => { + mkRetrySwitcher({resultIds: ['result-1']}); + + assert.notCalled(RetrySwitcherItem); + }); + + it('should create tab switcher buttons for each result', () => { + mkRetrySwitcher({resultIds: ['result-1', 'result-2'], retryIndex: 1}); + + assert.calledTwice(RetrySwitcherItem); + assert.calledWith( + RetrySwitcherItem.firstCall, + {children: 1, resultId: 'result-1', isActive: false, onClick: sinon.match.func} + ); + assert.calledWith( + RetrySwitcherItem.secondCall, + {children: 2, resultId: 'result-2', isActive: true, onClick: sinon.match.func} + ); + }); + + it('should call "onChange" prop on call passed "onClick" prop', () => { + const onChange = sinon.stub(); + mkRetrySwitcher({resultIds: ['result-1', 'result-2'], onChange}); + + RetrySwitcherItem.secondCall.args[0].onClick(); + + assert.calledOnceWith(onChange, 1); + }); +}); diff --git a/test/unit/lib/static/components/section/body/retry-switcher/item.js b/test/unit/lib/static/components/section/body/retry-switcher/item.js new file mode 100644 index 000000000..cfd100035 --- /dev/null +++ b/test/unit/lib/static/components/section/body/retry-switcher/item.js @@ -0,0 +1,64 @@ +import React from 'react'; +import {defaults} from 'lodash'; +import RetrySwitcherItem from 'lib/static/components/section/body/retry-switcher/item'; +import {FAIL, SUCCESS} from 'lib/constants/test-statuses'; +import {mkConnectedComponent} from '../../../utils'; + +describe('<RetrySwitcherItem />', () => { + const sandbox = sinon.sandbox.create(); + + const mkRetrySwitcherItem = (props = {}, initialState = {}) => { + props = defaults(props, { + resultId: 'default-id', + isActive: true, + onClick: sinon.stub() + }); + + initialState = defaults(initialState, { + tree: { + results: { + byId: { + 'default-id': {status: SUCCESS} + } + } + } + }); + + return mkConnectedComponent(<RetrySwitcherItem {...props} />, {initialState}); + }; + + afterEach(() => sandbox.restore()); + + it('should render button with status class name', () => { + const initialState = { + tree: { + results: { + byId: { + 'result-1': {status: FAIL} + } + } + } + }; + + const component = mkRetrySwitcherItem({resultId: 'result-1', isActive: true}, initialState); + + assert.lengthOf(component.find('.tab-switcher__button'), 1); + assert.lengthOf(component.find(`.tab-switcher__button_status_${FAIL}`), 1); + }); + + it('should render button with correct active class name', () => { + const component = mkRetrySwitcherItem({isActive: true}); + + assert.lengthOf(component.find('.tab-switcher__button'), 1); + assert.lengthOf(component.find('.tab-switcher__button_active'), 1); + }); + + it('should call "onClick" handler on click in button', () => { + const onClick = sinon.stub(); + + const component = mkRetrySwitcherItem({onClick}); + component.find('.tab-switcher__button').simulate('click'); + + assert.calledOnceWith(onClick); + }); +}); diff --git a/test/unit/lib/static/components/section/body/tabs.js b/test/unit/lib/static/components/section/body/tabs.js new file mode 100644 index 000000000..8ee7174ce --- /dev/null +++ b/test/unit/lib/static/components/section/body/tabs.js @@ -0,0 +1,82 @@ +import React from 'react'; +import proxyquire from 'proxyquire'; +import {defaultsDeep} from 'lodash'; +import {ERROR, SUCCESS} from 'lib/constants/test-statuses'; + +describe('<Tabs />', () => { + const sandbox = sinon.sandbox.create(); + let Tabs, State; + + const mkTabs = (props = {}) => { + props = defaultsDeep(props, { + result: { + id: 'default-result-id', + status: SUCCESS, + imageIds: [], + multipleTabs: true, + screenshot: true + } + }); + + return mount(<Tabs {...props} />); + }; + + beforeEach(() => { + State = sinon.stub().returns(null); + + Tabs = proxyquire('lib/static/components/section/body/tabs', { + '../../state': {default: State} + }).default; + }); + + afterEach(() => sandbox.restore()); + + it('should not render image tabs if images does not exist and test passed successfully', () => { + const result = {status: SUCCESS, imageIds: []}; + + const component = mkTabs({result}); + + assert.lengthOf(component.find('.tab'), 0); + assert.notCalled(State); + }); + + it('should render error tab if test failed without images', () => { + const result = {status: ERROR, imageIds: []}; + + const component = mkTabs({result}); + + assert.lengthOf(component.find('.tab'), 1); + assert.calledOnceWith(State, {result, imageId: null}); + }); + + it('should render image tab for each image id', () => { + const result = {status: ERROR, imageIds: ['img-1', 'img-2']}; + + const component = mkTabs({result}); + + assert.lengthOf(component.find('.tab'), 2); + assert.calledTwice(State); + assert.calledWith(State.firstCall, {result, imageId: 'img-1'}); + assert.calledWith(State.secondCall, {result, imageId: 'img-2'}); + }); + + it('should not render additional error tab if test errored with screenshot on reject', () => { + const result = {status: ERROR, imageIds: ['img-1'], screenshot: true}; + + const component = mkTabs({result}); + + assert.lengthOf(component.find('.tab'), 1); + assert.calledOnceWith(State, {result, imageId: 'img-1'}); + }); + + it('should render additional error tab if test errored without screenshot on reject', () => { + const result = {status: ERROR, imageIds: ['img-1'], screenshot: false}; + + const component = mkTabs({result}); + + assert.lengthOf(component.find('.tab'), 2); + assert.calledTwice(State); + assert.calledWith(State.firstCall, {result, imageId: 'img-1'}); + assert.calledWith(State.secondCall, {result, imageId: null}); + }); +}); diff --git a/test/unit/lib/static/components/section/section-browser.js b/test/unit/lib/static/components/section/section-browser.js index 1f4c9dd68..dee5d2b8e 100644 --- a/test/unit/lib/static/components/section/section-browser.js +++ b/test/unit/lib/static/components/section/section-browser.js @@ -1,37 +1,89 @@ import React from 'react'; import {defaults} from 'lodash'; -import SectionBrowser from 'lib/static/components/section/section-browser'; -import {SKIPPED, ERROR} from 'lib/constants/test-statuses'; -import {mkConnectedComponent, mkTestResult_} from '../utils'; -import {mkSuite, mkBrowserResult} from '../../../../utils'; +import proxyquire from 'proxyquire'; +import {SUCCESS, SKIPPED, ERROR} from 'lib/constants/test-statuses'; +import {mkConnectedComponent} from '../utils'; describe('<SectionBrowser/>', () => { - const mkSectionBrowserComponent = (sectionBrowserProps = {}, initialState = {}) => { - const browser = sectionBrowserProps.browser || mkBrowserResult(); + const sandbox = sinon.sandbox.create(); + let SectionBrowser, Body, selectors, hasBrowserFailedRetries, shouldBrowserBeShown; + + const mkBrowser = (opts) => { + const browser = defaults(opts, { + id: 'default-bro-id', + name: 'default-bro', + parentId: 'default-test-id', + resultIds: [] + }); + + return {[browser.id]: browser}; + }; + + const mkResult = (opts) => { + const result = defaults(opts, { + id: 'default-result-id', + parentId: 'default-bro-id', + status: SUCCESS, + imageIds: [] + }); + + return {[result.id]: result}; + }; + + const mkStateTree = ({browsersById = {}, resultsById = {}} = {}) => { + return { + browsers: {byId: browsersById}, + results: {byId: resultsById} + }; + }; - sectionBrowserProps = defaults(sectionBrowserProps, { - suite: mkSuite(), - browser + const mkSectionBrowserComponent = (props = {}, initialState = {}) => { + props = defaults(props, { + browserId: 'default-bro-id', + errorGroupBrowserIds: [] + }); + initialState = defaults(initialState, { + tree: mkStateTree(), + view: {expand: 'all'} }); - return mkConnectedComponent(<SectionBrowser {...sectionBrowserProps} />, {initialState}); + return mkConnectedComponent(<SectionBrowser {...props} />, {initialState}); }; + beforeEach(() => { + Body = sandbox.stub().returns(null); + hasBrowserFailedRetries = sandbox.stub().returns(false); + shouldBrowserBeShown = sandbox.stub().returns(true); + selectors = { + mkHasBrowserFailedRetries: sandbox.stub().returns(hasBrowserFailedRetries), + mkShouldBrowserBeShown: sandbox.stub().returns(shouldBrowserBeShown) + }; + + SectionBrowser = proxyquire('lib/static/components/section/section-browser', { + '../../modules/selectors/tree': selectors, + './body': {default: Body} + }).default; + }); + + afterEach(() => sandbox.restore()); + describe('skipped test', () => { it('should render "[skipped]" tag in title', () => { - const testResult = mkTestResult_({status: SKIPPED}); - const browser = mkBrowserResult({name: 'yabro', result: testResult}); + const browsersById = mkBrowser({id: 'yabro-1', name: 'yabro', resultIds: ['res'], parentId: 'test'}); + const resultsById = mkResult({id: 'res', status: SKIPPED}); + const tree = mkStateTree({browsersById, resultsById}); - const component = mkSectionBrowserComponent({browser}); + const component = mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); assert.equal(component.find('.section__title_skipped').first().text(), `[${SKIPPED}] yabro`); }); it('should render skip reason', () => { - const testResult = mkTestResult_({status: SKIPPED, skipReason: 'some-reason'}); - const browser = mkBrowserResult({name: 'yabro', result: testResult}); + const browsersById = mkBrowser({id: 'yabro-1', name: 'yabro', resultIds: ['res'], parentId: 'test'}); + const resultsById = mkResult({id: 'res', status: SKIPPED, skipReason: 'some-reason'}); + const tree = mkStateTree({browsersById, resultsById}); - const component = mkSectionBrowserComponent({browser}); + const component = mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); assert.equal( component.find('.section__title_skipped').first().text(), @@ -40,65 +92,100 @@ describe('<SectionBrowser/>', () => { }); it('should not render body even if all tests expanded', () => { - const testResult = mkTestResult_({status: SKIPPED}); - const browser = mkBrowserResult({name: 'yabro', result: testResult}); + const browsersById = mkBrowser({id: 'yabro-1', name: 'yabro', resultIds: ['res'], parentId: 'test'}); + const resultsById = mkResult({id: 'res', status: SKIPPED}); + const tree = mkStateTree({browsersById, resultsById}); - const component = mkSectionBrowserComponent({browser}, {view: {expand: 'all'}}); + mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree, view: {expand: 'all'}}); - assert.lengthOf(component.find('.section__body'), 0); + assert.notCalled(Body); }); }); describe('executed test with fails in retries and skip in result', () => { it('should render not skipped title', () => { - const retries = [mkTestResult_({status: ERROR, error: {}})]; - const testResult = mkTestResult_({status: SKIPPED, skipReason: 'some-reason'}); - const browser = mkBrowserResult({name: 'yabro', result: testResult, retries}); + const browsersById = mkBrowser( + {id: 'yabro-1', name: 'yabro', resultIds: ['res-1', 'res-2'], parentId: 'test'} + ); + const resultsById = { + ...mkResult({id: 'res-1', status: ERROR, error: {}}), + ...mkResult({id: 'res-2', status: SKIPPED, skipReason: 'some-reason'}) + }; + + const tree = mkStateTree({browsersById, resultsById}); + hasBrowserFailedRetries.returns(true); - const component = mkSectionBrowserComponent({browser}); + const component = mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); assert.equal(component.find('.section__title').first().text(), `[${SKIPPED}] yabro, reason: some-reason`); assert.isFalse(component.find('.section__title').exists('.section__title_skipped')); }); it('should not render body if only errored tests expanded', () => { - const retries = [mkTestResult_({status: ERROR, error: {}})]; - const testResult = mkTestResult_({status: SKIPPED}); - const browser = mkBrowserResult({name: 'yabro', result: testResult, retries}); + const browsersById = mkBrowser( + {id: 'yabro-1', name: 'yabro', resultIds: ['res-1', 'res-2'], parentId: 'test'} + ); + const resultsById = { + ...mkResult({id: 'res-1', status: ERROR, error: {}}), + ...mkResult({id: 'res-2', status: SKIPPED}) + }; + + const tree = mkStateTree({browsersById, resultsById}); + hasBrowserFailedRetries.returns(true); - const component = mkSectionBrowserComponent({browser}, {view: {expand: 'errors'}}); + mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree, view: {expand: 'errors'}}); - assert.lengthOf(component.find('.section__body'), 0); + assert.notCalled(Body); }); it('should render body if all tests expanded', () => { - const retries = [mkTestResult_({status: ERROR, error: {}})]; - const testResult = mkTestResult_({status: SKIPPED}); - const browser = mkBrowserResult({name: 'yabro', result: testResult, retries}); + const browsersById = mkBrowser( + {id: 'yabro-1', name: 'yabro', resultIds: ['res-1', 'res-2'], parentId: 'test'} + ); + const resultsById = { + ...mkResult({id: 'res-1', status: ERROR, error: {}}), + ...mkResult({id: 'res-2', status: SKIPPED}) + }; - const component = mkSectionBrowserComponent({browser}, {view: {expand: 'all'}}); + const tree = mkStateTree({browsersById, resultsById}); + hasBrowserFailedRetries.returns(true); - assert.lengthOf(component.find('.section__body'), 1); + mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree, view: {expand: 'all'}}); + + assert.calledOnceWith( + Body, + {browserId: 'yabro-1', browserName: 'yabro', testName: 'test', resultIds: ['res-1', 'res-2']} + ); }); }); describe('should render body for executed skipped test', () => { it('with error and without retries', () => { - const testResult = mkTestResult_({status: SKIPPED, error: {}}); - const browser = mkBrowserResult({name: 'yabro', result: testResult}); + const browsersById = mkBrowser({id: 'yabro-1', name: 'yabro', resultIds: ['res-1'], parentId: 'test'}); + const resultsById = mkResult({id: 'res-1', status: SKIPPED, error: {}}); + + const tree = mkStateTree({browsersById, resultsById}); + hasBrowserFailedRetries.returns(false); - const component = mkSectionBrowserComponent({browser}, {view: {expand: 'all'}}); + mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree, view: {expand: 'all'}}); - assert.lengthOf(component.find('.section__body'), 1); + assert.calledOnceWith( + Body, {browserId: 'yabro-1', browserName: 'yabro', testName: 'test', resultIds: ['res-1']} + ); }); it('with existed images and without retries', () => { - const testResult = mkTestResult_({status: SKIPPED, imagesInfo: [{}]}); - const browser = mkBrowserResult({name: 'yabro', result: testResult}); + const browsersById = mkBrowser({id: 'yabro-1', name: 'yabro', resultIds: ['res-1'], parentId: 'test'}); + const resultsById = mkResult({id: 'res-1', status: SKIPPED, error: {}, imageIds: ['img-1']}); + + const tree = mkStateTree({browsersById, resultsById}); + hasBrowserFailedRetries.returns(false); - const component = mkSectionBrowserComponent({browser}, {view: {expand: 'all'}}); + mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree, view: {expand: 'all'}}); - assert.lengthOf(component.find('.section__body'), 1); + assert.calledOnceWith( + Body, {browserId: 'yabro-1', browserName: 'yabro', testName: 'test', resultIds: ['res-1']} + ); }); }); }); diff --git a/test/unit/lib/static/components/section/section-common.js b/test/unit/lib/static/components/section/section-common.js index f8b2587de..1bbc2b4f4 100644 --- a/test/unit/lib/static/components/section/section-common.js +++ b/test/unit/lib/static/components/section/section-common.js @@ -1,35 +1,171 @@ import React from 'react'; -import SectionCommon from 'lib/static/components/section/section-common'; +import {defaults} from 'lodash'; +import proxyquire from 'proxyquire'; +import LazilyRender from '@gemini-testing/react-lazily-render'; import {FAIL, SUCCESS} from 'lib/constants/test-statuses'; -import {mkConnectedComponent, mkTestResult_} from '../utils'; -import {mkBrowserResult, mkSuiteTree} from '../../../../utils'; +import {mkConnectedComponent} from '../utils'; describe('<SectionCommon/>', () => { + const sandbox = sinon.sandbox.create(); + let SectionCommon, Title, SectionBrowser, selectors, hasSuiteFailedRetries, shouldSuiteBeShown; + + const mkSuite = (opts) => { + const result = defaults(opts, { + id: 'default-suite-id', + parentId: null, + name: 'default-name', + status: SUCCESS, + browserIds: [] + }); + + return {[result.id]: result}; + }; + + const mkStateTree = ({suitesById = {}} = {}) => { + return { + suites: {byId: suitesById} + }; + }; + const mkSectionCommonComponent = (props = {}, initialState = {}) => { + props = defaults(props, { + suiteId: 'default-suite-id', + eventToUpdate: '', + errorGroupBrowserIds: [] + }); + initialState = defaults(initialState, { + tree: mkStateTree(), + view: {expand: 'all'} + }); + return mkConnectedComponent(<SectionCommon {...props} />, {initialState}); }; + beforeEach(() => { + SectionBrowser = sandbox.stub().returns(null); + Title = sandbox.stub().returns(null); + hasSuiteFailedRetries = sandbox.stub().returns(false); + shouldSuiteBeShown = sandbox.stub().returns(true); + + selectors = { + mkHasSuiteFailedRetries: sandbox.stub().returns(hasSuiteFailedRetries), + mkShouldSuiteBeShown: sandbox.stub().returns(shouldSuiteBeShown) + }; + + SectionCommon = proxyquire('lib/static/components/section/section-common', { + '../../modules/selectors/tree': selectors, + './section-browser': {default: SectionBrowser}, + './title/simple': {default: Title} + }).default; + }); + + afterEach(() => sandbox.restore()); + describe('expand retries', () => { - it('should not render body section if suite has not failed retries', () => { - const retries = [mkTestResult_({status: SUCCESS})]; - const result = mkTestResult_({status: SUCCESS}); - const bro = mkBrowserResult({retries, result}); - const suite = mkSuiteTree({browsers: [bro]}); + it('should not render browser section if suite has not failed retries', () => { + const suitesById = mkSuite({id: 'suite-1', name: 'suite', status: SUCCESS}); + const tree = mkStateTree({suitesById}); + hasSuiteFailedRetries.returns(false); + + mkSectionCommonComponent({suiteId: 'suite-1'}, {tree, view: {expand: 'retries'}}); + + assert.notCalled(SectionBrowser); + }); + + it('should render browser section if suite has failed retries', () => { + const suitesById = mkSuite({id: 'suite-1', name: 'suite', status: FAIL, browserIds: ['bro-1']}); + const tree = mkStateTree({suitesById}); + hasSuiteFailedRetries.returns(true); - const component = mkSectionCommonComponent({suite}, {view: {expand: 'retries'}}); + mkSectionCommonComponent({suiteId: 'suite-1', errorGroupBrowserIds: []}, {tree, view: {expand: 'retries'}}); - assert.lengthOf(component.find('.section__body'), 0); + assert.calledOnceWith(SectionBrowser, {browserId: 'bro-1', errorGroupBrowserIds: []}); }); + }); + + describe('lazy renderer', () => { + it('should not wrap root suite if "lazyLoadOffset" is disabled', () => { + const suitesById = mkSuite({id: 'suite-1'}); + const tree = mkStateTree({suitesById}); + + const component = mkSectionCommonComponent( + {suiteId: 'suite-1', sectionRoot: true}, + {tree, view: {lazyLoadOffset: 0}} + ); + + assert.lengthOf(component.find(LazilyRender), 0); + }); + + describe('if "lazyLoadOffset" is enabled', () => { + it('should wrap only root suite if "lazyLoadOffset" is enabled', () => { + const suitesById = { + ...mkSuite({id: 'suite-1', suiteIds: ['suite-2']}), + ...mkSuite({id: 'suite-2', browserIds: ['bro-1']}) + }; + const tree = mkStateTree({suitesById}); + + const component = mkSectionCommonComponent( + {suiteId: 'suite-1', sectionRoot: true}, + {tree, view: {lazyLoadOffset: 100500}} + ); + + assert.lengthOf(component.find(LazilyRender), 1); + }); + + it('should pass "offset" prop to lazy wrapper', () => { + const suitesById = mkSuite({id: 'suite-1'}); + const tree = mkStateTree({suitesById}); + + const component = mkSectionCommonComponent( + {suiteId: 'suite-1', sectionRoot: true}, + {tree, view: {lazyLoadOffset: 100500}} + ); + const lazyOffset = component.find(LazilyRender).prop('offset'); + + assert.equal(lazyOffset, 100500); + }); + + it('should pass "eventToUpdate" prop to update suite', () => { + const suitesById = mkSuite({id: 'suite-1'}); + const tree = mkStateTree({suitesById}); + + const component = mkSectionCommonComponent( + {suiteId: 'suite-1', sectionRoot: true, eventToUpdate: 'some-event'}, + {tree, view: {lazyLoadOffset: 100500}} + ); + const lazyEventToUpdate = component.find(LazilyRender).prop('eventToUpdate'); + + assert.equal(lazyEventToUpdate, 'some-event'); + }); + + it('should pass empty "content" prop if suite is not rendered yet', () => { + const suitesById = mkSuite({id: 'suite-1'}); + const tree = mkStateTree({suitesById}); + + const component = mkSectionCommonComponent( + {suiteId: 'suite-1', sectionRoot: true, eventToUpdate: 'some-event'}, + {tree, view: {lazyLoadOffset: 100500}} + ); + const lazyContent = component.find(LazilyRender).prop('content'); + + assert.isNull(lazyContent); + }); + + it('should pass section to "content" prop if suite is rendered', () => { + const suitesById = mkSuite({id: 'suite-1'}); + const tree = mkStateTree({suitesById}); - it('should render body section if suite has failed retries', () => { - const retries = [mkTestResult_({status: FAIL})]; - const result = mkTestResult_({status: SUCCESS}); - const bro = mkBrowserResult({retries, result}); - const suite = mkSuiteTree({browsers: [bro]}); + const component = mkSectionCommonComponent( + {suiteId: 'suite-1', sectionRoot: true, eventToUpdate: 'some-event'}, + {tree, view: {lazyLoadOffset: 100500}} + ); + const lazy = component.find(LazilyRender); + lazy.invoke('onRender')(); - const component = mkSectionCommonComponent({suite}, {view: {expand: 'retries'}}); + const lazyContent = component.find(LazilyRender).prop('content'); - assert.isAtLeast(component.find('.section__body').length, 1); + assert.isNotNull(lazyContent); + }); }); }); }); diff --git a/test/unit/lib/static/components/section/state.js b/test/unit/lib/static/components/section/state.js deleted file mode 100644 index 7dcbe3496..000000000 --- a/test/unit/lib/static/components/section/state.js +++ /dev/null @@ -1,327 +0,0 @@ -import React from 'react'; -import proxyquire from 'proxyquire'; -import {defaults, defaultsDeep} from 'lodash'; -import {SUCCESS, FAIL, ERROR, UPDATED, IDLE} from 'lib/constants/test-statuses'; -import StateError from 'lib/static/components/state/state-error'; -import {mkConnectedComponent, mkTestResult_, mkImg_} from '../utils'; - -describe('<State/>', () => { - const sandbox = sinon.sandbox.create(); - - let State; - let utilsStub; - - const mkToggleHandler = (testResult) => { - return sandbox.stub().callsFake(({opened}) => { - testResult.opened = opened; - }); - }; - - const mkStateComponent = (stateProps = {}, initialState = {}) => { - const state = stateProps.state || mkTestResult_(); - - stateProps = defaults(stateProps, { - state, - acceptHandler: () => {}, - findSameDiffsHandler: () => {}, - toggleHandler: () => {} - }); - - initialState = defaultsDeep(initialState, { - gui: true, - config: {errorPatterns: []}, - view: {expand: 'all', scaleImages: false, showOnlyDiff: false, lazyLoadOffset: 0} - }); - - return mkConnectedComponent( - <State {...stateProps} />, - {initialState} - ); - }; - - beforeEach(() => { - utilsStub = {isAcceptable: sandbox.stub()}; - - State = proxyquire('lib/static/components/state', { - '../../modules/utils': utilsStub - }).default; - }); - - afterEach(() => sandbox.restore()); - - [ - {name: 'accept', text: '✔ Accept'}, - {name: 'find same diffs', text: '🔍 Find same diffs'} - ].forEach(({name, text}, ind) => { - it(`should render ${name} button if "gui" is running`, () => { - const stateComponent = mkStateComponent({}, {gui: true}); - - assert.equal(stateComponent.find('.button_type_suite-controls').at(ind).text(), text); - }); - - it(`should not render ${name} button if "gui" is not running`, () => { - const stateComponent = mkStateComponent({}, {gui: false}); - - assert.lengthOf(stateComponent.find('.button_type_suite-controls'), 0); - }); - }); - - describe('"Accept" button', () => { - it('should be disabled if test result is not acceptable', () => { - const testResult = mkTestResult_({status: IDLE}); - utilsStub.isAcceptable.withArgs(testResult).returns(false); - - const stateComponent = mkStateComponent({state: testResult}); - - assert.isTrue(stateComponent.find('[label="✔ Accept"]').prop('isDisabled')); - }); - - it('should be enabled if test result is acceptable', () => { - const testResult = mkTestResult_(); - utilsStub.isAcceptable.withArgs(testResult).returns(true); - - const stateComponent = mkStateComponent({state: testResult}); - - assert.isFalse(stateComponent.find('[label="✔ Accept"]').prop('isDisabled')); - }); - - it('should call accept handler on click', () => { - const testResult = mkTestResult_({name: 'bro'}); - const acceptHandler = sinon.stub(); - - utilsStub.isAcceptable.withArgs(testResult).returns(true); - - const stateComponent = mkStateComponent({state: testResult, acceptHandler}); - - stateComponent.find('[label="✔ Accept"]').simulate('click'); - - assert.calledOnce(acceptHandler); - }); - }); - - describe('"Find same diffs" button', () => { - it('should be disabled if test result is errored', () => { - const testResult = mkTestResult_({status: ERROR}); - - const stateComponent = mkStateComponent({state: testResult, error: {}}); - - assert.isTrue(stateComponent.find('[label="🔍 Find same diffs"]').prop('isDisabled')); - }); - - it('should be disabled if test result is success', () => { - const testResult = mkTestResult_({status: SUCCESS}); - - const stateComponent = mkStateComponent({state: testResult}); - - assert.isTrue(stateComponent.find('[label="🔍 Find same diffs"]').prop('isDisabled')); - }); - - it('should be enabled if test result is failed', () => { - const testResult = mkTestResult_({status: FAIL, actualImg: mkImg_(), diffImg: mkImg_()}); - - const stateComponent = mkStateComponent({state: testResult}); - - assert.isFalse(stateComponent.find('[label="🔍 Find same diffs"]').prop('isDisabled')); - }); - - it('should call find same diffs handler on click', () => { - const testResult = mkTestResult_({status: FAIL, actualImg: mkImg_(), diffImg: mkImg_()}); - const findSameDiffsHandler = sinon.stub(); - - const stateComponent = mkStateComponent({state: testResult, findSameDiffsHandler}); - - stateComponent.find('[label="🔍 Find same diffs"]').simulate('click'); - - assert.calledOnce(findSameDiffsHandler); - }); - }); - - describe('scaleImages', () => { - it('should not scale images by default', () => { - const testResult = mkTestResult_({status: FAIL, actualImg: mkImg_(), diffImg: mkImg_()}); - const stateComponent = mkStateComponent({state: testResult}); - const imageContainer = stateComponent.find('.image-box__container'); - - assert.isFalse(imageContainer.hasClass('image-box__container_scale')); - }); - - it('should scale images if "scaleImages" option is enabled', () => { - const testResult = mkTestResult_({status: FAIL, actualImg: mkImg_(), diffImg: mkImg_()}); - const stateComponent = mkStateComponent({state: testResult}, {view: {scaleImages: true}}); - const imageContainer = stateComponent.find('.image-box__container'); - - assert.isTrue(imageContainer.hasClass('image-box__container_scale')); - }); - }); - - describe('lazyLoad', () => { - it('should load images lazy if "lazyLoadOffset" is specified', () => { - const testResult = mkTestResult_({status: SUCCESS}); - const stateComponent = mkStateComponent({state: testResult}, {view: {lazyLoadOffset: 800}}); - const lazyLoadContainer = stateComponent.find('LazyLoad'); - - assert.lengthOf(lazyLoadContainer, 1); - }); - - describe('should not load images lazy', () => { - it('if passed image contain "size" but "lazyLoadOffset" does not set', () => { - const expectedImg = mkImg_({size: {width: 200, height: 100}}); - const testResult = mkTestResult_({status: SUCCESS, expectedImg}); - - const stateComponent = mkStateComponent({state: testResult}); - const lazyLoadContainer = stateComponent.find('LazyLoad'); - - assert.lengthOf(lazyLoadContainer, 0); - }); - - describe('if passed image does not contain "size" and "lazyLoadOffset" is', () => { - it('set to 0', () => { - const testResult = mkTestResult_({status: SUCCESS, expectedImg: {path: 'some/path'}}); - - const stateComponent = mkStateComponent({state: testResult}, {view: {lazyLoadOffset: 0}}); - const lazyLoadContainer = stateComponent.find('LazyLoad'); - - assert.lengthOf(lazyLoadContainer, 0); - }); - - it('not specified', () => { - const testResult = mkTestResult_({status: SUCCESS, expectedImg: {path: 'some/path'}}); - - const stateComponent = mkStateComponent({state: testResult}); - const lazyLoadContainer = stateComponent.find('LazyLoad'); - - assert.lengthOf(lazyLoadContainer, 0); - }); - }); - }); - - describe('should render placeholder with', () => { - it('"width" prop equal to passed image width', () => { - const expectedImg = mkImg_({size: {width: 200}}); - const testResult = mkTestResult_({status: SUCCESS, expectedImg}); - - const stateComponent = mkStateComponent({state: testResult}, {view: {lazyLoadOffset: 10}}); - - assert.equal(stateComponent.find('Placeholder').prop('width'), 200); - }); - - it('"paddingTop" prop calculated depending on width and height of the image', () => { - const expectedImg = mkImg_({size: {width: 200, height: 100}}); - const testResult = mkTestResult_({status: SUCCESS, expectedImg}); - - const stateComponent = mkStateComponent({state: testResult}, {view: {lazyLoadOffset: 10}}); - - assert.equal(stateComponent.find('Placeholder').prop('paddingTop'), '50.00%'); - }); - }); - }); - - describe('should show opened state if', () => { - ['errors', 'retries'].forEach((expand) => { - it(`"${expand}" expanded and test failed`, () => { - const testResult = mkTestResult_({ - status: FAIL, stateName: 'plain', actualImg: mkImg_(), diffImg: mkImg_() - }); - const toggleHandler = mkToggleHandler(testResult); - - const stateComponent = mkStateComponent({state: testResult, toggleHandler}, {view: {expand}}); - - assert.lengthOf(stateComponent.find('.image-box__container'), 1); - }); - - it(`"${expand}" expanded and test errored`, () => { - const testResult = mkTestResult_({status: ERROR, stateName: 'plain'}); - const toggleHandler = mkToggleHandler(testResult); - - const stateComponent = mkStateComponent({state: testResult, toggleHandler, error: {}}, {view: {expand}}); - - assert.lengthOf(stateComponent.find('.image-box__container'), 1); - }); - }); - - it('"all" expanded and test success', () => { - const testResult = mkTestResult_({status: SUCCESS, stateName: 'plain'}); - const toggleHandler = mkToggleHandler(testResult); - - const stateComponent = mkStateComponent({state: testResult, toggleHandler}, {view: {expand: 'all'}}); - - assert.lengthOf(stateComponent.find('.image-box__container'), 1); - }); - - it('stateName is not specified', () => { - const testResult = mkTestResult_({status: SUCCESS}); - - const stateComponent = mkStateComponent({state: testResult}, {view: {expand: 'errors'}}); - - assert.lengthOf(stateComponent.find('.image-box__container'), 1); - }); - }); - - describe('should not show opened state if', () => { - ['errors', 'retries'].forEach((expand) => { - it(`"${expand}" expanded and test success`, () => { - const testResult = mkTestResult_({status: SUCCESS, stateName: 'plain'}); - - const stateComponent = mkStateComponent({state: testResult}, {view: {expand}}); - - assert.lengthOf(stateComponent.find('.image-box__container'), 0); - }); - - it(`"${expand}" expanded and test updated`, () => { - const testResult = mkTestResult_({status: UPDATED, stateName: 'plain'}); - - const stateComponent = mkStateComponent({state: testResult}, {view: {expand}}); - - assert.lengthOf(stateComponent.find('.image-box__container'), 0); - }); - }); - }); - - it('should open closed state by click on it', () => { - const testResult = mkTestResult_({status: SUCCESS, stateName: 'plain'}); - const toggleHandler = mkToggleHandler(testResult); - - const stateComponent = mkStateComponent({state: testResult, toggleHandler}, {view: {expand: 'errors'}}); - stateComponent.find('.state-title').simulate('click'); - - assert.lengthOf(stateComponent.find('.image-box__container'), 1); - }); - - describe('"toggleHandler" handler', () => { - it('should call on mount', () => { - const testResult = mkTestResult_({stateName: 'plain'}); - const toggleHandler = mkToggleHandler(testResult); - - mkStateComponent({state: testResult, toggleHandler}); - - assert.calledOnceWith(toggleHandler, {stateName: 'plain', opened: true}); - }); - - it('should call on click in state name', () => { - const testResult = mkTestResult_({stateName: 'plain'}); - const toggleHandler = mkToggleHandler(testResult); - - const stateComponent = mkStateComponent({state: testResult, toggleHandler}); - stateComponent.find('.state-title').simulate('click'); - - assert.calledTwice(toggleHandler); - assert.calledWith(toggleHandler.firstCall, {stateName: 'plain', opened: true}); - assert.calledWith(toggleHandler.secondCall, {stateName: 'plain', opened: false}); - }); - }); - - [ERROR, FAIL].forEach((status) => { - describe(`<StateError/> with ${status} status`, () => { - it('should render with first matched error pattern', () => { - const error = {message: 'foo-bar'}; - const errorPatterns = [{name: 'first', regexp: /foo-bar/}, {name: 'second', regexp: /foo-bar/}]; - const testResult = mkTestResult_({status, stateName: 'plain', opened: true}); - - const stateComponent = mkStateComponent({state: testResult, error}, {config: {errorPatterns}}); - - assert.lengthOf(stateComponent.find(StateError), 1); - assert.deepEqual(stateComponent.find(StateError).prop('errorPattern'), errorPatterns[0]); - }); - }); - }); -}); diff --git a/test/unit/lib/static/components/section/switcher-retry.js b/test/unit/lib/static/components/section/switcher-retry.js deleted file mode 100644 index 31614be95..000000000 --- a/test/unit/lib/static/components/section/switcher-retry.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import SwitcherRetry from 'lib/static/components/section/switcher-retry'; -import {FAIL, ERROR, UPDATED, SUCCESS} from 'lib/constants/test-statuses'; -import {mkTestResult_} from '../utils'; - -describe('SwitcherRetry component', () => { - const mkSwitcherRetry = (props = {}) => { - props = {testResults: [], onChange: () => {}, ...props}; - - return mount(<SwitcherRetry testResults={props.testResults} onChange={props.onChange} />); - }; - - it('should not render any tab switcher button if test did not retry', () => { - const testResults = [mkTestResult_({status: FAIL})]; - - const component = mkSwitcherRetry({testResults}); - - assert.lengthOf(component.find('.tab-switcher__button'), 0); - }); - - it('should create tab switcher buttons with status of each test result', () => { - const testResults = [ - mkTestResult_({status: FAIL}), mkTestResult_({status: ERROR}), - mkTestResult_({status: UPDATED}), mkTestResult_({status: SUCCESS}) - ]; - - const component = mkSwitcherRetry({testResults}); - - assert.lengthOf(component.find('.tab-switcher__button'), 4); - assert.lengthOf(component.find(`.tab-switcher__button_status_${FAIL}`), 1); - assert.lengthOf(component.find(`.tab-switcher__button_status_${ERROR}`), 1); - assert.lengthOf(component.find(`.tab-switcher__button_status_${UPDATED}`), 1); - assert.lengthOf(component.find(`.tab-switcher__button_status_${SUCCESS}`), 1); - }); -}); diff --git a/test/unit/lib/static/components/section/diff-circle.js b/test/unit/lib/static/components/state/diff-circle.js similarity index 100% rename from test/unit/lib/static/components/section/diff-circle.js rename to test/unit/lib/static/components/state/diff-circle.js diff --git a/test/unit/lib/static/components/state/index.js b/test/unit/lib/static/components/state/index.js new file mode 100644 index 000000000..2d8c14dda --- /dev/null +++ b/test/unit/lib/static/components/state/index.js @@ -0,0 +1,374 @@ +import React from 'react'; +import proxyquire from 'proxyquire'; +import {defaults, defaultsDeep} from 'lodash'; +import {SUCCESS, FAIL, ERROR, UPDATED} from 'lib/constants/test-statuses'; +import {mkConnectedComponent} from '../utils'; + +describe('<State/>', () => { + const sandbox = sinon.sandbox.create(); + let State, StateError, StateSuccess, StateFail, FindSameDiffsButton, utilsStub, actionsStub; + + const mkStateComponent = (props = {}, initialState = {}) => { + props = defaults(props, { + result: {status: SUCCESS}, + imageId: 'default-img-id' + }); + + initialState = defaultsDeep(initialState, { + tree: { + images: { + byId: { + 'default-img-id': { + stateName: 'default-state', + expectedImg: {}, + status: SUCCESS + } + }, + stateById: { + 'default-img-id': {opend: false} + } + } + }, + view: {expand: 'all'} + }); + + return mkConnectedComponent(<State {...props} />, {initialState}); + }; + + beforeEach(() => { + utilsStub = {isAcceptable: sandbox.stub()}; + + actionsStub = { + toggleStateResult: sandbox.stub().returns({type: 'some-type'}), + acceptTest: sandbox.stub().returns({type: 'some-type'}) + }; + + StateError = sinon.stub().returns(null); + StateSuccess = sinon.stub().returns(null); + StateFail = sinon.stub().returns(null); + FindSameDiffsButton = sinon.stub().returns(null); + + State = proxyquire('lib/static/components/state', { + './state-error': {default: StateError}, + './state-success': {default: StateSuccess}, + './state-fail': {default: StateFail}, + '../controls/find-same-diffs-button': {default: FindSameDiffsButton}, + '../../modules/actions': actionsStub, + '../../modules/utils': utilsStub + }).default; + }); + + afterEach(() => sandbox.restore()); + + describe('"Accept" button', () => { + it('should not render button in static report', () => { + const stateComponent = mkStateComponent({}, {gui: false}); + + assert.lengthOf(stateComponent.find('.button_type_suite-controls'), 0); + }); + + it('should render button in gui report', () => { + const stateComponent = mkStateComponent({}, {gui: true}); + + assert.lengthOf(stateComponent.find('.button_type_suite-controls'), 1); + }); + + it('should be disabled if image is not acceptable', () => { + const image = {stateName: 'some-name', status: SUCCESS}; + const initialState = { + tree: { + images: { + byId: {'img-id': image}, + stateById: {'img-id': {opened: false}} + } + } + }; + utilsStub.isAcceptable.withArgs(image).returns(false); + + const stateComponent = mkStateComponent({imageId: 'img-id'}, initialState); + + assert.isTrue(stateComponent.find('[label="✔ Accept"]').prop('isDisabled')); + }); + + it('should be enabled if image is acceptable', () => { + const image = {stateName: 'some-name', status: FAIL}; + const initialState = { + tree: { + images: { + byId: {'img-id': image}, + stateById: {'img-id': {opened: true}} + } + } + }; + utilsStub.isAcceptable.withArgs(image).returns(true); + + const stateComponent = mkStateComponent({imageId: 'img-id'}, initialState); + + assert.isFalse(stateComponent.find('[label="✔ Accept"]').prop('isDisabled')); + }); + + it('should call "acceptTest" action on button click', () => { + const image = {stateName: 'some-name', status: FAIL}; + const initialState = { + tree: { + images: { + byId: {'img-id': image}, + stateById: {'img-id': {opened: true}} + } + } + }; + utilsStub.isAcceptable.withArgs(image).returns(true); + + const stateComponent = mkStateComponent({imageId: 'img-id'}, initialState); + stateComponent.find('[label="✔ Accept"]').simulate('click'); + + assert.calledOnceWith(actionsStub.acceptTest, 'img-id'); + }); + }); + + describe('"FindSameDiffsButton" button', () => { + it('should not render button in static report', () => { + mkStateComponent({}, {gui: false}); + + assert.notCalled(FindSameDiffsButton); + }); + + it('should render button in gui report', () => { + mkStateComponent({}, {gui: true}); + + assert.calledOnce(FindSameDiffsButton); + }); + + it('should be disabled if image is not failed', () => { + const image = {stateName: 'some-name', status: SUCCESS}; + const initialState = { + tree: { + images: { + byId: {'img-id': image}, + stateById: {'img-id': {opened: true}} + } + } + }; + const result = {parentId: 'bro-id', status: SUCCESS}; + mkStateComponent({result, imageId: 'img-id'}, initialState); + + assert.calledOnceWith(FindSameDiffsButton, {imageId: 'img-id', browserId: 'bro-id', isDisabled: true}); + }); + + it('should be enabled if image is failed', () => { + const image = {stateName: 'some-name', status: FAIL}; + const initialState = { + tree: { + images: { + byId: {'img-id': image}, + stateById: {'img-id': {opened: true}} + } + } + }; + const result = {parentId: 'bro-id', status: FAIL}; + mkStateComponent({result, imageId: 'img-id'}, initialState); + + assert.calledOnceWith(FindSameDiffsButton, {imageId: 'img-id', browserId: 'bro-id', isDisabled: false}); + }); + }); + + describe('scaleImages', () => { + it('should not scale images by default', () => { + const stateComponent = mkStateComponent(); + const imageContainer = stateComponent.find('.image-box__container'); + + assert.isFalse(imageContainer.hasClass('image-box__container_scale')); + }); + + it('should scale images if "scaleImages" option is enabled', () => { + const stateComponent = mkStateComponent({}, {view: {scaleImages: true}}); + const imageContainer = stateComponent.find('.image-box__container'); + + assert.isTrue(imageContainer.hasClass('image-box__container_scale')); + }); + }); + + describe('should show opened state if', () => { + ['errors', 'retries'].forEach((expand) => { + it(`"${expand}" expanded and test failed`, () => { + const image = {stateName: 'some-name', status: FAIL}; + const initialState = { + tree: { + images: { + byId: {'img-id': image} + } + }, + view: {expand} + }; + const result = {status: FAIL}; + + const stateComponent = mkStateComponent({result, imageId: 'img-id'}, initialState); + + assert.lengthOf(stateComponent.find('.image-box__container'), 1); + }); + + it(`"${expand}" expanded and test errored`, () => { + const image = {stateName: 'some-name', status: ERROR}; + const initialState = { + tree: { + images: { + byId: {'img-id': image} + } + }, + view: {expand} + }; + const result = {status: ERROR}; + + const stateComponent = mkStateComponent({result, imageId: 'img-id'}, initialState); + + assert.lengthOf(stateComponent.find('.image-box__container'), 1); + }); + }); + + it('"all" expanded and test success', () => { + const image = {stateName: 'some-name', status: SUCCESS}; + const initialState = { + tree: { + images: { + byId: {'img-id': image} + } + }, + view: {expand: 'all'} + }; + const result = {status: SUCCESS}; + + const stateComponent = mkStateComponent({result, imageId: 'img-id'}, initialState); + + assert.lengthOf(stateComponent.find('.image-box__container'), 1); + }); + + it('stateName is not specified', () => { + const image = {stateName: null, status: SUCCESS}; + const initialState = { + tree: { + images: { + byId: {'img-id': image} + } + }, + view: {expand: 'errors'} + }; + const result = {status: SUCCESS}; + + const stateComponent = mkStateComponent({result, imageId: 'img-id'}, initialState); + + assert.lengthOf(stateComponent.find('.image-box__container'), 1); + }); + }); + + describe('should not show opened state if', () => { + ['errors', 'retries'].forEach((expand) => { + it(`"${expand}" expanded and test success`, () => { + const image = {stateName: 'some-name', status: SUCCESS}; + const initialState = { + tree: { + images: { + byId: {'img-id': image} + } + }, + view: {expand} + }; + const result = {status: SUCCESS}; + + const stateComponent = mkStateComponent({result, imageId: 'img-id'}, initialState); + + assert.lengthOf(stateComponent.find('.image-box__container'), 0); + }); + + it(`"${expand}" expanded and test updated`, () => { + const image = {stateName: 'some-name', status: UPDATED}; + const initialState = { + tree: { + images: { + byId: {'img-id': image} + } + }, + view: {expand} + }; + const result = {status: UPDATED}; + + const stateComponent = mkStateComponent({result, imageId: 'img-id'}, initialState); + + assert.lengthOf(stateComponent.find('.image-box__container'), 0); + }); + }); + }); + + it('should open closed state by click on it', () => { + const image = {stateName: 'some-name', status: SUCCESS}; + const initialState = { + tree: { + images: { + byId: {'img-id': image} + } + }, + view: {expand: 'errors'} + }; + const result = {status: SUCCESS}; + + const stateComponent = mkStateComponent({result, imageId: 'img-id'}, initialState); + stateComponent.find('.state-title').simulate('click'); + + assert.lengthOf(stateComponent.find('.image-box__container'), 1); + }); + + describe('"toggleStateResult" action', () => { + it('should call on mount', () => { + const image = {stateName: 'some-name', status: SUCCESS}; + const initialState = { + tree: { + images: { + byId: {'img-id': image} + } + }, + view: {expand: 'all'} + }; + + mkStateComponent({imageId: 'img-id'}, initialState); + + assert.calledOnceWith(actionsStub.toggleStateResult, {imageId: 'img-id', opened: true}); + }); + + it('should call on click in state name', () => { + const image = {stateName: 'some-name', status: SUCCESS}; + const initialState = { + tree: { + images: { + byId: {'img-id': image} + } + }, + view: {expand: 'all'} + }; + + const stateComponent = mkStateComponent({imageId: 'img-id'}, initialState); + stateComponent.find('.state-title').simulate('click'); + + assert.calledTwice(actionsStub.toggleStateResult); + assert.calledWith(actionsStub.toggleStateResult.firstCall, {imageId: 'img-id', opened: true}); + assert.calledWith(actionsStub.toggleStateResult.secondCall, {imageId: 'img-id', opened: false}); + }); + + it('should call on unmount', () => { + const image = {stateName: 'some-name', status: SUCCESS}; + const initialState = { + tree: { + images: { + byId: {'img-id': image} + } + }, + view: {expand: 'all'} + }; + + const stateComponent = mkStateComponent({imageId: 'img-id'}, initialState); + stateComponent.unmount(); + + assert.calledTwice(actionsStub.toggleStateResult); + assert.calledWith(actionsStub.toggleStateResult.firstCall, {imageId: 'img-id', opened: true}); + assert.calledWith(actionsStub.toggleStateResult.secondCall, {imageId: 'img-id', opened: false}); + }); + }); +}); diff --git a/test/unit/lib/static/components/section/screenshot.js b/test/unit/lib/static/components/state/screenshot.js similarity index 77% rename from test/unit/lib/static/components/section/screenshot.js rename to test/unit/lib/static/components/state/screenshot.js index 55685cd11..35bfd0627 100644 --- a/test/unit/lib/static/components/section/screenshot.js +++ b/test/unit/lib/static/components/state/screenshot.js @@ -79,4 +79,24 @@ describe('Screenshot component', () => { assert.lengthOf(screenshotComponent.find(LazyLoad), 0); }); }); + + describe('should render placeholder with', () => { + it('"width" prop equal to passed image width', () => { + const screenshotComponent = mkConnectedComponent( + <Screenshot image={{path: '', size: {width: 200}}} />, + {initialState: {view: {lazyLoadOffset: 100}}} + ); + + assert.equal(screenshotComponent.find(Placeholder).prop('width'), 200); + }); + + it('"paddingTop" prop calculated depending on width and height of the image', () => { + const screenshotComponent = mkConnectedComponent( + <Screenshot image={{path: '', size: {width: 200, height: 100}}} />, + {initialState: {view: {lazyLoadOffset: 10}}} + ); + + assert.equal(screenshotComponent.find(Placeholder).prop('paddingTop'), '50.00%'); + }); + }); }); diff --git a/test/unit/lib/static/components/section/state-error.js b/test/unit/lib/static/components/state/state-error.js similarity index 55% rename from test/unit/lib/static/components/section/state-error.js rename to test/unit/lib/static/components/state/state-error.js index 3dcbe1a36..74322b9f8 100644 --- a/test/unit/lib/static/components/section/state-error.js +++ b/test/unit/lib/static/components/state/state-error.js @@ -1,25 +1,43 @@ import React from 'react'; -import StateError from 'lib/static/components/state/state-error'; +import {defaults} from 'lodash'; +import proxyquire from 'proxyquire'; import {ERROR_TITLE_TEXT_LENGTH} from 'lib/constants/errors'; +import {mkConnectedComponent} from '../utils'; describe('<StateError/> component', () => { - const mkStateErrorComponent = (props = {}) => { - props = { - image: false, - error: {message: 'default-message', stack: 'default-stack'}, - actualImg: {}, - errorPattern: null, - ...props - }; - - return mount(<StateError {...props} />); + const sandbox = sinon.sandbox.create(); + let StateError, Screenshot; + + const mkStateErrorComponent = (props = {}, initialState = {}) => { + props = defaults(props, { + result: { + error: {message: 'default-message', stack: 'default-stack'} + }, + image: {stateName: 'some-name'} + }); + + initialState = defaults(initialState, { + config: {errorPatterns: []} + }); + + return mkConnectedComponent(<StateError {...props} />, {initialState}); }; - describe('"errorPattern" prop is not passed', () => { - it('should render error "message", "stack" and "history" if "errorPattern" prop is not passed', () => { + beforeEach(() => { + Screenshot = sinon.stub().returns(null); + + StateError = proxyquire('lib/static/components/state/state-error', { + './screenshot': {default: Screenshot} + }).default; + }); + + 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'}; - const component = mkStateErrorComponent({error}); + 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'); @@ -29,7 +47,7 @@ describe('<StateError/> component', () => { it('should break error fields by line break', () => { const error = {message: 'msg-title\nmsg-content'}; - const component = mkStateErrorComponent({error}); + const component = mkStateErrorComponent({result: {error}}, {config: {errorPatterns: []}}); assert.equal(component.find('.details__summary').first().text(), 'message: msg-title'); assert.equal(component.find('.details__content').first().text(), 'msg-content'); @@ -38,7 +56,7 @@ describe('<StateError/> component', () => { it(`should not break error fields if theirs length is less than ${ERROR_TITLE_TEXT_LENGTH} charachters`, () => { const error = {message: Array(ERROR_TITLE_TEXT_LENGTH - 1).join('a')}; - const component = mkStateErrorComponent({error}); + const component = mkStateErrorComponent({result: {error}}, {config: {errorPatterns: []}}); assert.equal(component.find('.error__item').first().text(), `message: ${error.message}`); }); @@ -48,19 +66,19 @@ describe('<StateError/> component', () => { const messageContent = Array(ERROR_TITLE_TEXT_LENGTH).join('b'); const error = {message: `${messageTitle} ${messageContent}`}; - const component = mkStateErrorComponent({error}); + const component = mkStateErrorComponent({result: {error}}, {config: {errorPatterns: []}}); assert.equal(component.find('.details__summary').first().text(), `message: ${messageTitle}`); assert.equal(component.find('.details__content').first().text(), messageContent); }); }); - describe('"errorPattern" prop is passed', () => { + describe('"errorPatterns" is specified', () => { it('should render error "message" with starting "name" of "errorPattern" prop', () => { const error = {message: 'some-msg'}; - const errorPattern = {name: 'some-name'}; + const errorPatterns = [{name: 'some-name'}]; - const component = mkStateErrorComponent({error, errorPattern}); + const component = mkStateErrorComponent({result: {error}}, {config: {errorPatterns}}); assert.equal(component.find('.details__summary').first().text(), 'message: some-name'); assert.equal(component.find('.details__content').first().text(), 'some-msg'); @@ -68,20 +86,20 @@ describe('<StateError/> component', () => { it('should render "hint" as plain string', () => { const error = {message: 'some-msg'}; - const errorPattern = {name: 'some-name', hint: 'some-hint'}; + const errorPatterns = [{name: 'some-name', hint: 'some-hint'}]; - const component = mkStateErrorComponent({error, errorPattern}); + const component = mkStateErrorComponent({result: {error}}, {config: {errorPatterns}}); assert.equal(component.find('.error__item').last().text(), 'hint: some-hint'); }); it('should render "hint" as html string', () => { const error = {message: 'some-msg'}; - const errorPattern = {name: 'some-name', hint: '<span class="foo-bar">some-hint</span>'}; + const errorPatterns = [{name: 'some-name', hint: '<span class="foo-bar">some-hint</span>'}]; - const component = mkStateErrorComponent({error, errorPattern}); + const component = mkStateErrorComponent({result: {error}}, {config: {errorPatterns}}); - assert.equal(component.find('.details__summary').last().text(), 'hint: show more'); + assert.equal(component.find('.details__summary').at(1).text(), 'hint: show more'); assert.equal(component.find('.details__content .foo-bar').text(), ['some-hint']); }); }); diff --git a/test/unit/lib/static/components/suites.js b/test/unit/lib/static/components/suites.js index aec2d5dfe..b565e1274 100644 --- a/test/unit/lib/static/components/suites.js +++ b/test/unit/lib/static/components/suites.js @@ -1,66 +1,85 @@ -import React, {Component} from 'react'; -import LazilyRender from '@gemini-testing/react-lazily-render'; +import React from 'react'; import proxyquire from 'proxyquire'; -import {defaultsDeep} from 'lodash'; +import {defaults, defaultsDeep} from 'lodash'; import {mkConnectedComponent} from './utils'; -import {mkState} from '../../../utils'; import {config} from 'lib/constants/defaults'; import clientEvents from 'lib/constants/client-events'; import viewModes from 'lib/constants/view-modes'; describe('<Suites/>', () => { - let Suites; - const SectionCommonStub = class SectionCommonStub extends Component { - render() { - return <div></div>; - } - }; + const sandbox = sinon.sandbox.create(); + let Suites, SectionCommon, selectors, getVisibleRootSuiteIds; + + const mkSuitesComponent = (props = {}, initialState = {}) => { + props = defaults(props, { + errorGroupBrowserIds: [] + }); - const mkSuitesComponent = (initialState = {}) => { initialState = defaultsDeep(initialState, { - gui: false, - suiteIds: {all: ['suite1']}, - suites: {'suite1': mkState({ - suitePath: ['suite1'] - })}, - view: {viewMode: viewModes.ALL, filteredBrowsers: [], lazyLoadOffset: config.lazyLoadOffset} + tree: { + suites: { + allRootIds: ['default-root-id'], + failedRootIds: [] + } + }, + view: {viewMode: viewModes.ALL, lazyLoadOffset: config.lazyLoadOffset} }); - return mkConnectedComponent(<Suites />, {initialState}); + return mkConnectedComponent(<Suites {...props} />, {initialState}); }; beforeEach(() => { + SectionCommon = sinon.stub().returns(null); + getVisibleRootSuiteIds = sinon.stub().returns([]); + + selectors = { + mkGetVisibleRootSuiteIds: sandbox.stub().returns(getVisibleRootSuiteIds) + }; + Suites = proxyquire('lib/static/components/suites', { - './section/section-common': { - default: SectionCommonStub - } + './section/section-common': {default: SectionCommon}, + '../modules/selectors/tree': selectors }).default; }); - it('should wrap suite with Lazy-renderer component by default', () => { - const suitesComponent = mkSuitesComponent(); + afterEach(() => sandbox.restore()); - assert.lengthOf(suitesComponent.find(LazilyRender), 1); + it('should not render section common component if there are not visible root suite ids', () => { + getVisibleRootSuiteIds.returns([]); + + mkSuitesComponent(); + + assert.notCalled(SectionCommon); }); - it('should pass to Lazy-renderer component "lazyLoadOffset" option', () => { - const suitesComponent = mkSuitesComponent({view: {lazyLoadOffset: 100}}); - const lazyRendererProps = suitesComponent.find(LazilyRender).props(); + it('should render section common without "eventToUpdate" if "lazyLoadOffset" disabled', () => { + getVisibleRootSuiteIds.returns(['suite-id']); + + mkSuitesComponent({errorGroupBrowserIds: []}, {view: {lazyLoadOffset: 0}}); - assert.equal(lazyRendererProps.offset, 100); + assert.calledOnceWith( + SectionCommon, + {suiteId: 'suite-id', sectionRoot: true, errorGroupBrowserIds: []} + ); }); - it('should pass to Lazy-renderer component event name to update suite', () => { - const suitesComponent = mkSuitesComponent(); - const lazyRendererProps = suitesComponent.find(LazilyRender).props(); + it('should render section common without "eventToUpdate" if "lazyLoadOffset" enabled', () => { + getVisibleRootSuiteIds.returns(['suite-id']); - assert.equal(lazyRendererProps.eventToUpdate, clientEvents.VIEW_CHANGED); + mkSuitesComponent({errorGroupBrowserIds: []}, {view: {lazyLoadOffset: 100500}}); + + assert.calledOnceWith( + SectionCommon, + {suiteId: 'suite-id', eventToUpdate: clientEvents.VIEW_CHANGED, sectionRoot: true, errorGroupBrowserIds: []} + ); }); - it('should not wrap suite with Lazy-renderer component if lazyLoadOffset was disabled', () => { - const suitesComponent = mkSuitesComponent({view: {lazyLoadOffset: 0}}); + it('should render few section commons components', () => { + getVisibleRootSuiteIds.returns(['suite-id-1', 'suite-id-2']); + + mkSuitesComponent(); - assert.lengthOf(suitesComponent.find(LazilyRender), 0); + assert.calledTwice(SectionCommon); }); }); diff --git a/test/unit/lib/static/components/utils.js b/test/unit/lib/static/components/utils.js index 8509e5da3..a89aa61aa 100644 --- a/test/unit/lib/static/components/utils.js +++ b/test/unit/lib/static/components/utils.js @@ -4,15 +4,19 @@ import configureStore from 'redux-mock-store'; import {Provider} from 'react-redux'; import defaultState from 'lib/static/modules/default-state'; -exports.mkStore = (state) => { - const initialState = {reporter: _.defaults(state, defaultState)}; +exports.mkState = ({initialState} = {}) => { + return _.defaultsDeep(initialState, defaultState); +}; + +exports.mkStore = ({initialState, state} = {}) => { + const readyState = state ? state : exports.mkState({initialState}); const mockStore = configureStore(); - return mockStore(initialState); + return mockStore(readyState); }; -exports.mkConnectedComponent = (Component, {initialState} = {}) => { - const store = exports.mkStore(initialState); +exports.mkConnectedComponent = (Component, state) => { + const store = exports.mkStore(state); return mount(<Provider store={store}>{Component}</Provider>); }; diff --git a/test/unit/lib/static/modules/actions.js b/test/unit/lib/static/modules/actions.js index 7a427763e..61b8934f4 100644 --- a/test/unit/lib/static/modules/actions.js +++ b/test/unit/lib/static/modules/actions.js @@ -1,91 +1,57 @@ -'use strict'; - import axios from 'axios'; -import {isArray} from 'lodash'; import proxyquire from 'proxyquire'; - -import { - acceptOpened, - retryTest, - runFailedTests -} from 'lib/static/modules/actions'; +import {acceptOpened, retryTest, runFailedTests} from 'lib/static/modules/actions'; import actionNames from 'lib/static/modules/action-names'; -import { - mkSuiteTree, - mkSuite, - mkState, - mkBrowserResult, - mkImagesInfo, - mkTestResult -} from '../../../utils'; -import {SUCCESS, FAIL} from 'lib/constants/test-statuses'; - -const mkBrowserResultWithImagesInfo = (name, status = FAIL) => { - return mkBrowserResult({ - name, - status, - result: mkTestResult({ - name, - status, - imagesInfo: [ - mkImagesInfo({ - status, - opened: true - }) - ] - }), - state: { - opened: true, - retryIndex: 0 - } - }); -}; +import StaticTestsTreeBuilder from 'lib/tests-tree-builder/static'; describe('lib/static/modules/actions', () => { const sandbox = sinon.sandbox.create(); - let dispatch; - let actions; - let addNotification; + let dispatch, actions, addNotification, getSuitesTableRows; beforeEach(() => { - dispatch = sinon.stub(); + dispatch = sandbox.stub(); sandbox.stub(axios, 'post').resolves({data: {}}); - addNotification = sinon.stub(); + addNotification = sandbox.stub(); + getSuitesTableRows = sandbox.stub(); + + sandbox.stub(StaticTestsTreeBuilder, 'create').returns(Object.create(StaticTestsTreeBuilder.prototype)); + sandbox.stub(StaticTestsTreeBuilder.prototype, 'build').returns({}); actions = proxyquire('lib/static/modules/actions', { - 'reapop': {addNotification} + 'reapop': {addNotification}, + './database-utils': {getSuitesTableRows} }); }); afterEach(() => sandbox.restore()); - describe('initial', () => { + describe('initGuiReport', () => { it('should run init action on server', async () => { sandbox.stub(axios, 'get').resolves({data: {}}); - await actions.initial()(dispatch); + await actions.initGuiReport()(dispatch); assert.calledOnceWith(axios.get, '/init'); }); - it('should dispatch "VIEW_INITIAL" action', async () => { + it('should dispatch "INIT_GUI_REPORT" action', async () => { sandbox.stub(axios, 'get').resolves({data: 'some-data'}); - await actions.initial()(dispatch); + await actions.initGuiReport()(dispatch); - assert.calledOnceWith(dispatch, {type: actionNames.VIEW_INITIAL, payload: 'some-data'}); + assert.calledOnceWith(dispatch, {type: actionNames.INIT_GUI_REPORT, payload: 'some-data'}); }); it('should show notification if error in initialization on the server is happened', async () => { sandbox.stub(axios, 'get').throws(new Error('failed to initialize custom gui')); - await actions.initial()(dispatch); + await actions.initGuiReport()(dispatch); assert.calledOnceWith( addNotification, { dismissAfter: 0, - id: 'initial', + id: 'initGuiReport', message: 'failed to initialize custom gui', status: 'error' } @@ -94,106 +60,44 @@ describe('lib/static/modules/actions', () => { }); describe('acceptOpened', () => { - it('should update reference for suite with children and browsers', async () => { - const failed = [ - mkSuite({ - suitePath: ['suite1'], - children: [ - mkSuite({ - suitePath: ['suite1', 'suite2'], - children: [ - mkState({ - suitePath: ['suite1', 'suite2', 'state'], - status: FAIL, - browsers: [mkBrowserResultWithImagesInfo('chrome')] - }) - ], - browsers: [mkBrowserResultWithImagesInfo('yabro')] - }) - ] - }) - ]; + it('should update opened images', async () => { + const imageIds = ['img-id-1', 'img-id-2']; + const images = [{id: 'img-id-1'}, {id: 'img-id-2'}]; + axios.post.withArgs('/get-update-reference-data', imageIds).returns({data: images}); - await acceptOpened(failed)(dispatch); + await acceptOpened(imageIds)(dispatch); - assert.calledWith( - axios.post, - sinon.match.any, - sinon.match(formattedFails => { - assert.lengthOf(formattedFails, 2); - assert.equal(formattedFails[0].browserId, 'yabro'); - assert.equal(formattedFails[1].browserId, 'chrome'); - return true; - }) - ); + assert.calledWith(axios.post.firstCall, '/get-update-reference-data', imageIds); + assert.calledWith(axios.post.secondCall, '/update-reference', images); }); }); describe('retryTest', () => { - const suite = mkSuite({ - suitePath: ['suite1'], - browsers: [ - mkBrowserResultWithImagesInfo('yabro', SUCCESS), - mkBrowserResultWithImagesInfo('foo-bar', FAIL) - ], - children: [ - mkState({ - suitePath: ['suite1', 'suite2'], - status: FAIL, - browsers: [ - mkBrowserResultWithImagesInfo('chrome'), - mkBrowserResultWithImagesInfo('yabro') - ] - }) - ] - }); + it('should retry passed test', async () => { + const test = {testName: 'test-name', browserName: 'yabro'}; - [ - {browserId: 'yabro', status: 'successful'}, - {browserId: 'foo-bar', status: 'failed'} - ].forEach(({browserId, status}) => { - it(`should run only ${status} test if it was passed`, async () => { - await retryTest(suite, browserId)(dispatch); - - assert.calledWith( - axios.post, - sinon.match.any, - sinon.match(tests => { - if (isArray(tests)) { - assert.equal(tests[0].browserId, browserId); - } else { - assert.equal(tests.browserId, browserId); - } - - return true; - }) - ); - }); + await retryTest(test)(dispatch); + + assert.calledOnceWith(axios.post, '/run', [test]); + assert.calledOnceWith(dispatch, {type: actionNames.RETRY_TEST}); }); }); describe('runFailedTests', () => { it('should run all failed tests', async () => { - const tests = mkSuiteTree({ - browsers: [ - {state: {opened: false}, result: {status: FAIL}}, - {state: {opened: true}, result: {status: SUCCESS}}, - {state: {opened: true}, result: {status: FAIL}}, - {result: {status: FAIL}} - ] - }); + const failedTests = [ + {testName: 'test-name-1', browserName: 'yabro'}, + {testName: 'test-name-2', browserName: 'yabro'} + ]; - await runFailedTests(tests)(dispatch); + await runFailedTests(failedTests)(dispatch); - assert.calledOnceWith(axios.post, sinon.match.any, sinon.match((formattedFails) => { - assert.lengthOf(formattedFails, 1); - assert.lengthOf(formattedFails[0].browsers, 3); - return true; - })); + assert.calledOnceWith(axios.post, '/run', failedTests); + assert.calledOnceWith(dispatch, {type: actionNames.RUN_FAILED_TESTS}); }); }); - describe('openDbConnection', () => { + describe('initStaticReport', () => { let fetchDatabasesStub; let mergeDatabasesStub; let actions; @@ -225,7 +129,7 @@ describe('lib/static/modules/actions', () => { it('should fetch databaseUrls.json for default html page', async () => { global.window.location.href = 'http://127.0.0.1:8080/'; - await actions.openDbConnection()(dispatch); + await actions.initStaticReport()(dispatch); assert.calledOnceWith(fetchDatabasesStub, ['http://127.0.0.1:8080/databaseUrls.json']); }); @@ -233,7 +137,7 @@ describe('lib/static/modules/actions', () => { it('should fetch databaseUrls.json for custom html page', async () => { global.window.location.href = 'http://127.0.0.1:8080/some/path.html'; - await actions.openDbConnection()(dispatch); + await actions.initStaticReport()(dispatch); assert.calledOnceWith(fetchDatabasesStub, ['http://127.0.0.1:8080/some/databaseUrls.json']); }); @@ -241,13 +145,14 @@ describe('lib/static/modules/actions', () => { it('should dispatch empty payload if fetchDatabases rejected', async () => { fetchDatabasesStub.rejects('stub'); - await actions.openDbConnection()(dispatch); + await actions.initStaticReport()(dispatch); - assert.calledOnceWith( - dispatch, - sinon.match({ - payload: {db: null, fetchDbDetails: []} - }), + assert.calledWith( + dispatch.lastCall, + { + type: actionNames.INIT_STATIC_REPORT, + payload: sinon.match({db: null, fetchDbDetails: []}) + } ); }); @@ -255,23 +160,44 @@ describe('lib/static/modules/actions', () => { fetchDatabasesStub.resolves([{url: 'stub url', status: 200, data: 'stub'}]); mergeDatabasesStub.rejects('stub'); - await actions.openDbConnection()(dispatch); + await actions.initStaticReport()(dispatch); - assert.calledOnceWith( - dispatch, - sinon.match({ - payload: {fetchDbDetails: [{url: 'stub url', status: 200, success: true}]} - }), + assert.calledWith( + dispatch.lastCall, + { + type: actionNames.INIT_STATIC_REPORT, + payload: sinon.match({fetchDbDetails: [{url: 'stub url', status: 200, success: true}]}) + } ); }); it('should filter null data before merge databases', async () => { fetchDatabasesStub.resolves([{url: 'stub url1', status: 404, data: null}, {url: 'stub url2', status: 200, data: 'stub'}]); - await actions.openDbConnection()(dispatch); + await actions.initStaticReport()(dispatch); assert.calledOnceWith(mergeDatabasesStub, ['stub']); }); + + it('should build tests tree', async () => { + const db = {}; + const suitesFromDb = ['rows-with-suites']; + const treeBuilderResult = {tree: {}, stats: {}, skips: {}, browsers: {}}; + + mergeDatabasesStub.resolves(db); + getSuitesTableRows.withArgs(db).returns(suitesFromDb); + StaticTestsTreeBuilder.prototype.build.withArgs(suitesFromDb).returns(treeBuilderResult); + + await actions.initStaticReport()(dispatch); + + assert.calledWith( + dispatch.lastCall, + { + type: actionNames.INIT_STATIC_REPORT, + payload: sinon.match({...treeBuilderResult}) + } + ); + }); }); describe('runCustomGuiAction', () => { diff --git a/test/unit/lib/static/modules/find-same-diffs-utils.js b/test/unit/lib/static/modules/find-same-diffs-utils.js deleted file mode 100644 index a53c90617..000000000 --- a/test/unit/lib/static/modules/find-same-diffs-utils.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -const { - getAllOpenedImagesInfo -} = require('lib/static/modules/find-same-diffs-utils'); -const { - mkSuite, - mkState, - mkBrowserResult, - mkImagesInfo, - mkTestResult -} = require('../../../utils'); -import {FAIL} from 'lib/constants/test-statuses'; - -describe('lib/static/modules/find-same-diffs-utils', () => { - describe('getAllOpenedImagesInfo', () => { - it('should get images info from suite with children and browsers', () => { - const mkBrowserResultWithImagesInfo = name => { - return mkBrowserResult({ - name, - result: mkTestResult({ - name, - imagesInfo: [ - mkImagesInfo({ - opened: true - }) - ] - }), - state: { - opened: true, - retryIndex: 0 - } - }); - }; - - const failed = [ - mkSuite({ - suitePath: ['suite1'], - children: [ - mkSuite({ - suitePath: ['suite1', 'suite2'], - children: [ - mkState({ - suitePath: ['suite1', 'suite2', 'state'], - status: FAIL, - browsers: [mkBrowserResultWithImagesInfo('chrome')] - }) - ], - browsers: [mkBrowserResultWithImagesInfo('yabro')] - }) - ] - }) - ]; - - const imagesInfo = getAllOpenedImagesInfo(failed); - - assert.lengthOf(imagesInfo, 2); - assert.deepEqual(imagesInfo[0].browserId, 'yabro'); - assert.deepEqual(imagesInfo[1].browserId, 'chrome'); - }); - }); -}); diff --git a/test/unit/lib/static/modules/group-errors.js b/test/unit/lib/static/modules/group-errors.js index 5c7112518..f77cc919b 100644 --- a/test/unit/lib/static/modules/group-errors.js +++ b/test/unit/lib/static/modules/group-errors.js @@ -1,613 +1,294 @@ 'use strict'; -const {groupErrors} = require('lib/static/modules/group-errors'); -const { - mkSuite, - mkState, - mkBrowserResult, - mkSuiteTree, - mkTestResult -} = require('../../../utils'); -const {mkImg_} = require('../components/utils'); +const proxyquire = require('proxyquire'); +const {defaults} = require('lodash'); +const {FAIL, SUCCESS} = require('lib/constants/test-statuses'); const viewModes = require('lib/constants/view-modes'); -function mkBrowserResultWithStatus(status) { - return mkBrowserResult({ - name: `${status} test`, - result: mkTestResult({ - status: status - }), - retries: [ - mkTestResult({ - error: { - message: `message stub ${status}` - } - }) - ] - }); -} - describe('static/modules/group-errors', () => { - it('should collect errors from all tests if viewMode is "all"', () => { - const suites = [ - mkSuiteTree({ - browsers: [ - mkBrowserResultWithStatus('skipped'), - mkBrowserResultWithStatus('success'), - mkBrowserResultWithStatus('fail'), - mkBrowserResultWithStatus('error') - ] - }) - ]; + const sandbox = sinon.sandbox.create(); + let groupErrors, shouldShowBrowser, isTestNameMatchFilters; + + const mkRootSuite = (opts) => { + const rootSuite = defaults(opts, { + id: 'default-root-suite-id', + parentId: null, + status: FAIL, + suiteIds: [] + }); - const result = groupErrors({suites, viewMode: viewModes.ALL}); + return {[rootSuite.id]: rootSuite}; + }; - assert.lengthOf(result, 4); - assert.include(result[0], { - count: 1, - name: 'message stub error' - }); - assert.include(result[1], { - count: 1, - name: 'message stub fail' + const mkSuite = (opts) => { + const suite = defaults(opts, { + id: 'default-suite-id', + parentId: 'default-root-suite-id', + status: FAIL, + browserIds: [] }); - assert.include(result[2], { - count: 1, - name: 'message stub skipped' - }); - assert.include(result[3], { - count: 1, - name: 'message stub success' + + return {[suite.id]: suite}; + }; + + const mkBrowser = (opts) => { + const browser = defaults(opts, { + id: 'default-bro-id', + parentId: 'default-test-id', + resultIds: [] }); - }); - it('should collect errors only from failed tests if viewMode is "failed"', () => { - const suites = [ - mkSuiteTree({ - suite: mkSuite({status: 'error'}), - browsers: [ - mkBrowserResultWithStatus('skipped'), - mkBrowserResultWithStatus('success'), - mkBrowserResultWithStatus('fail'), - mkBrowserResultWithStatus('error') - ] - }) - ]; + return {[browser.id]: browser}; + }; + + const mkResult = (opts) => { + const result = defaults(opts, { + id: 'default-result-id', + parentId: 'default-bro-id', + status: FAIL, + imageIds: [] + }); - const result = groupErrors({suites, viewMode: viewModes.FAILED}); + return {[result.id]: result}; + }; - assert.lengthOf(result, 2); - assert.include(result[0], { - count: 1, - name: 'message stub error' + const mkImage = (opts) => { + const image = defaults(opts, { + id: 'default-image-id', + status: FAIL }); - assert.include(result[1], { - count: 1, - name: 'message stub fail' + + return {[image.id]: image}; + }; + + const mkTree = ({suitesById = {}, failedRootIds = [], browsersById = {}, resultsById = {}, imagesById = {}} = {}) => { + return { + suites: {byId: suitesById, failedRootIds}, + browsers: {byId: browsersById}, + results: {byId: resultsById}, + images: {byId: imagesById} + }; + }; + + beforeEach(() => { + shouldShowBrowser = sandbox.stub().returns(true); + isTestNameMatchFilters = sandbox.stub().returns(true); + + const module = proxyquire('lib/static/modules/group-errors', { + './selectors/tree': {shouldShowBrowser}, + './utils': {isTestNameMatchFilters} }); + + groupErrors = module.groupErrors; }); - 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'}} - ] - }) - }) - ] - }) - ]; + afterEach(() => sandbox.restore()); - const result = groupErrors({suites}); + it('should collect errors from all tests if viewMode is "all"', () => { + const browsersById = { + ...mkBrowser({id: 'yabro-1', parentId: 'test-1'}), + ...mkBrowser({id: 'yabro-2', parentId: 'test-2'}) + }; + const resultsById = { + ...mkResult({id: 'res-1', parentId: 'yabro-1', error: {message: 'err-1'}}), + ...mkResult({id: 'res-2', parentId: 'yabro-2', error: {message: 'err-2'}}) + }; + const tree = mkTree({browsersById, resultsById}); + + const result = groupErrors({tree, viewMode: viewModes.ALL}); assert.deepEqual(result, [ { + pattern: 'err-1', + name: 'err-1', count: 1, - name: 'message stub first', - pattern: 'message stub first', - tests: { - 'default-suite default-state': ['default-bro'] - } + browserIds: ['yabro-1'] }, { + pattern: 'err-2', + name: 'err-2', count: 1, - name: 'message stub second', - pattern: 'message stub second', - tests: { - 'default-suite default-state': ['default-bro'] - } + browserIds: ['yabro-2'] } ]); }); - it('should collect image comparison fails', () => { - const suites = [ - mkSuiteTree({ - browsers: [ - mkBrowserResult({ - result: mkTestResult({ - imagesInfo: [{diffImg: mkImg_()}] - }) - }) - ] - }) - ]; - - const result = groupErrors({suites}); - - assert.lengthOf(result, 1); - assert.include(result[0], { + it('should collect errors only from failed tests if viewMode is "failed"', () => { + const suitesById = { + ...mkRootSuite({id: 'suite-1', status: FAIL, suiteIds: ['test-1', 'test-2']}), + ...mkSuite({id: 'test-1', status: FAIL, browserIds: ['yabro-1']}), + ...mkSuite({id: 'test-2', status: SUCCESS, browserIds: ['yabro-2']}) + }; + const failedRootIds = ['suite-1']; + const browsersById = { + ...mkBrowser({id: 'yabro-1', parentId: 'test-1', resultIds: ['res-1']}), + ...mkBrowser({id: 'yabro-2', parentId: 'test-2', resultIds: ['res-2']}) + }; + const resultsById = { + ...mkResult({id: 'res-1', parentId: 'yabro-1', status: FAIL, error: {message: 'err-1'}}), + ...mkResult({id: 'res-2', parentId: 'yabro-2', status: SUCCESS, error: {message: 'err-2'}}) + }; + const tree = mkTree({suitesById, failedRootIds, browsersById, resultsById}); + + const result = groupErrors({tree, viewMode: viewModes.FAILED}); + + assert.deepEqual(result, [{ + pattern: 'err-1', + name: 'err-1', count: 1, - name: 'image comparison failed', - pattern: 'image comparison failed' - }); + browserIds: ['yabro-1'] + }]); }); - 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' - } - }) - ] - }) - ] - }) - ]; + it('should collect errors from tests and images', () => { + const browsersById = { + ...mkBrowser({id: 'yabro-1', parentId: 'test-1'}) + }; + const resultsById = { + ...mkResult({id: 'res-1', parentId: 'yabro-1', imageIds: ['img-1'], error: {message: 'err'}}) + }; + const imagesById = { + ...mkImage({id: 'img-1', error: {message: 'img-err'}}) + }; + const tree = mkTree({browsersById, resultsById, imagesById}); - const result = groupErrors({suites}); + const result = groupErrors({tree}); assert.deepEqual(result, [ { + pattern: 'img-err', + name: 'img-err', count: 1, - name: 'message stub first', - pattern: 'message stub first', - tests: { - 'default-suite default-state': ['default-bro'] - } + browserIds: ['yabro-1'] }, { + pattern: 'err', + name: 'err', count: 1, - name: 'message stub second', - pattern: 'message stub second', - tests: { - 'default-suite default-state': ['default-bro'] - } + browserIds: ['yabro-1'] } ]); }); - 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 collect image comparison fails', () => { + const browsersById = { + ...mkBrowser({id: 'yabro-1', parentId: 'test-1'}) + }; + const resultsById = { + ...mkResult({id: 'res-1', parentId: 'yabro-1', imageIds: ['img-1']}) + }; + const imagesById = { + ...mkImage({id: 'img-1', diffImg: {}}) + }; + const tree = mkTree({browsersById, resultsById, imagesById}); + + const result = groupErrors({tree}); + + assert.deepEqual(result, [{ + pattern: 'image comparison failed', + name: 'image comparison failed', + count: 1, + browserIds: ['yabro-1'] + }]); }); - 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 group equal errors', () => { + const browsersById = { + ...mkBrowser({id: 'yabro-1', parentId: 'test-1'}), + ...mkBrowser({id: 'yabro-2', parentId: 'test-2'}) + }; + const resultsById = { + ...mkResult({id: 'res-1', parentId: 'yabro-1', error: {message: 'err'}}), + ...mkResult({id: 'res-2', parentId: 'yabro-2', error: {message: 'err'}}) + }; + const tree = mkTree({browsersById, resultsById}); + + const result = groupErrors({tree}); + + assert.deepEqual(result, [{ + pattern: 'err', + name: 'err', + count: 2, + browserIds: ['yabro-1', 'yabro-2'] + }]); }); 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 partial 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-o' - }); - - assert.deepEqual(result, [ - { - count: 1, - name: 'message stub', - pattern: 'message stub', - tests: { - 'suite state-one': ['default-bro'] - } - } - ]); - }); - - it('should filter by partial test name with strictMatchFilter', () => { - 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', - strictMatchFilter: true - }); - - assert.deepEqual(result, [ - { - count: 1, - name: 'message stub', - pattern: 'message stub', - tests: { - 'suite state-one': ['default-bro'] - } - } - ]); - }); - - it('should resolve to empty result by partial test name with strictMatchFilter', () => { - 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-on', - strictMatchFilter: true - }); - - assert.deepEqual(result, []); + const browsersById = { + ...mkBrowser({id: 'yabro-1', parentId: 'test-1'}), + ...mkBrowser({id: 'yabro-2', parentId: 'test-2'}) + }; + const resultsById = { + ...mkResult({id: 'res-1', parentId: 'yabro-1', error: {message: 'err-1'}}), + ...mkResult({id: 'res-2', parentId: 'yabro-2', error: {message: 'err-2'}}) + }; + const tree = mkTree({browsersById, resultsById}); + + isTestNameMatchFilters + .withArgs('test-1', 'test-1', true).returns(true) + .withArgs('test-2', 'test-1', true).returns(false); + + const result = groupErrors({tree, testNameFilter: 'test-1', strictMatchFilter: true}); + + assert.deepEqual(result, [{ + pattern: 'err-1', + name: 'err-1', + count: 1, + browserIds: ['yabro-1'] + }]); }); 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: [{id: 'browser-one'}] - }); - - assert.deepEqual(result, [ - { - count: 1, - name: 'message stub', - pattern: 'message stub', - tests: { - 'suite state': ['browser-one'] - } - } - ]); - }); - - it('should filter by browser with versions', () => { - const suites = [ - mkSuite({ - suitePath: ['suite'], - children: [ - mkSuite({ - suitePath: ['suite', 'state'], - browsers: [ - mkBrowserResult({ - name: 'browser-one', - result: mkTestResult({ - error: { - message: 'message stub' - } - }) - }), - mkBrowserResult({ - name: 'browser-one', - result: mkTestResult({ - browserVersion: '1.1', - error: { - message: 'message stub 1' - } - }) - }) - ] - }) - ] - }) - ]; - - const result = groupErrors({ - suites, - filteredBrowsers: [{id: 'browser-one', versions: ['1.1']}] - }); - - assert.deepEqual(result, [ - { - count: 1, - name: 'message stub 1', - pattern: 'message stub 1', - tests: { - 'suite state': ['browser-one'] - } - } - ]); + const browsersById = { + ...mkBrowser({id: 'yabro-1', parentId: 'test-1'}), + ...mkBrowser({id: 'yabro-2', parentId: 'test-2'}) + }; + const resultsById = { + ...mkResult({id: 'res-1', parentId: 'yabro-1', error: {message: 'err-1'}}), + ...mkResult({id: 'res-2', parentId: 'yabro-2', error: {message: 'err-2'}}) + }; + const tree = mkTree({browsersById, resultsById}); + const filteredBrowsers = ['yabro-1']; + + shouldShowBrowser + .withArgs(browsersById['yabro-1'], filteredBrowsers).returns(true) + .withArgs(browsersById['yabro-2'], filteredBrowsers).returns(false); + + const result = groupErrors({tree, filteredBrowsers}); + + assert.deepEqual(result, [{ + pattern: 'err-1', + name: 'err-1', + count: 1, + browserIds: ['yabro-1'] + }]); }); 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 browsersById = { + ...mkBrowser({id: 'yabro-1', parentId: 'test-1'}), + ...mkBrowser({id: 'yabro-2', parentId: 'test-2'}) + }; + const resultsById = { + ...mkResult({id: 'res-1', parentId: 'yabro-1', error: {message: 'err-1'}}), + ...mkResult({id: 'res-2', parentId: 'yabro-2', error: {message: 'err-2'}}) + }; + const tree = mkTree({browsersById, resultsById}); const errorPatterns = [ { - name: 'Name group: message stub first', - pattern: 'message .* first', - regexp: /message .* first/ + name: 'Name group: err', + pattern: 'err-.*', + regexp: /err-.*/ } ]; - const result = groupErrors({suites, errorPatterns}); + const result = groupErrors({tree, 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'] - } - } - ]); + assert.deepEqual(result, [{ + pattern: 'err-.*', + name: 'Name group: err', + count: 2, + browserIds: ['yabro-1', 'yabro-2'] + }]); }); }); diff --git a/test/unit/lib/static/modules/reducers/helpers/local-storage-wrapper.js b/test/unit/lib/static/modules/local-storage-wrapper.js similarity index 70% rename from test/unit/lib/static/modules/reducers/helpers/local-storage-wrapper.js rename to test/unit/lib/static/modules/local-storage-wrapper.js index d2fe63faf..37fe97894 100644 --- a/test/unit/lib/static/modules/reducers/helpers/local-storage-wrapper.js +++ b/test/unit/lib/static/modules/local-storage-wrapper.js @@ -1,8 +1,7 @@ -'use strict'; -import * as localStorageWrapper from 'lib/static/modules/reducers/helpers/local-storage-wrapper'; -import {mkStorage} from '../../../../../utils'; +import * as localStorageWrapper from 'lib/static/modules/local-storage-wrapper'; +import {mkStorage} from '../../../utils'; -describe('lib/static/modules/reducers/helpers/local-storage-wrapper', () => { +describe('lib/static/modules/local-storage-wrapper', () => { const prefix = 'html-reporter'; beforeEach(() => { @@ -13,18 +12,6 @@ describe('lib/static/modules/reducers/helpers/local-storage-wrapper', () => { global.window = undefined; }); - describe('updateDeprecatedKeys', () => { - it('should update storage keys if deprecated', () => { - global.window.localStorage.setItem('_gemini-replace-host', 'foo1'); - global.window.localStorage.setItem('foo2', 'foo2'); - - localStorageWrapper.updateDeprecatedKeys(); - - assert.equal(global.window.localStorage.getItem(`${prefix}:replace-host`), '"foo1"'); - assert.equal(global.window.localStorage.getItem('foo2'), 'foo2'); - }); - }); - describe('setItem', () => { it('should convert value to json and set value to localStorage', () => { localStorageWrapper.setItem('foo', {bar: []}); diff --git a/test/unit/lib/static/modules/middlewares/local-storage.js b/test/unit/lib/static/modules/middlewares/local-storage.js new file mode 100644 index 000000000..ffb3c7fc5 --- /dev/null +++ b/test/unit/lib/static/modules/middlewares/local-storage.js @@ -0,0 +1,68 @@ +import proxyquire from 'proxyquire'; +import actionNames from 'lib/static/modules/action-names'; +import defaultState from 'lib/static/modules/default-state'; +import viewModes from 'lib/constants/view-modes'; + +describe('lib/static/modules/middlewares/local-storage', () => { + const sandbox = sinon.sandbox.create(); + let next, localStorageWrapper; + + const mkStore_ = (state = {}) => { + return { + getState: sinon.stub().returns(state) + }; + }; + + const mkMiddleware_ = () => { + return proxyquire('lib/static/modules/middlewares/local-storage', { + '../local-storage-wrapper': localStorageWrapper + }).default; + }; + + beforeEach(() => { + next = sandbox.stub(); + + localStorageWrapper = { + setItem: sandbox.stub(), + getItem: sandbox.stub() + }; + }); + + afterEach(() => sandbox.restore()); + + it('should call next middleware by default', () => { + const store = mkStore_(); + const action = {type: 'FOO_BAR'}; + const localStorageMw = mkMiddleware_(); + + localStorageMw(store)(next)(action); + + assert.calledOnceWith(next, action); + }); + + [ + actionNames.INIT_GUI_REPORT, + actionNames.INIT_STATIC_REPORT, + 'VIEW_FOO_ACTION' + ].forEach((type) => { + describe(`"${type}" action`, () => { + it('should store view state in local storage for "VIEW" prefix and init report actions', () => { + const store = mkStore_({view: defaultState.view}); + const action = {type}; + const localStorageMw = mkMiddleware_(); + + localStorageMw(store)(next)(action); + + assert.calledOnceWith(localStorageWrapper.setItem, 'view', { + expand: 'errors', + groupByError: false, + scaleImages: false, + showOnlyDiff: false, + showSkipped: false, + strictMatchFilter: false, + viewMode: viewModes.ALL + }); + }); + }); + }); +}); diff --git a/test/unit/lib/static/modules/middlewares/metrika.js b/test/unit/lib/static/modules/middlewares/metrika.js index 6d80f7e9d..2f5b693c7 100644 --- a/test/unit/lib/static/modules/middlewares/metrika.js +++ b/test/unit/lib/static/modules/middlewares/metrika.js @@ -8,12 +8,12 @@ describe('lib/static/modules/middlewares/metrika', () => { const mkStore_ = (state = {}) => { return { - getState: sinon.stub().returns(state) + getState: sandbox.stub().returns(state) }; }; beforeEach(() => { - next = sinon.stub(); + next = sandbox.stub(); sandbox.stub(YandexMetrika, 'create').returns(Object.create(YandexMetrika.prototype)); sandbox.stub(YandexMetrika.prototype, 'acceptScreenshot'); @@ -31,10 +31,10 @@ describe('lib/static/modules/middlewares/metrika', () => { assert.calledOnceWith(next, action); }); - describe(`"${actionNames.VIEW_INITIAL}" event`, () => { + describe(`"${actionNames.INIT_GUI_REPORT}" event`, () => { it('should call next middleware with passed action', () => { const store = mkStore_(); - const action = {type: actionNames.VIEW_INITIAL}; + const action = {type: actionNames.INIT_GUI_REPORT}; metrikaMiddleware(YandexMetrika)(store)(next)(action); @@ -43,7 +43,7 @@ describe('lib/static/modules/middlewares/metrika', () => { it('should call next middleware before get state', () => { const store = mkStore_(); - const action = {type: actionNames.VIEW_INITIAL}; + const action = {type: actionNames.INIT_GUI_REPORT}; metrikaMiddleware(YandexMetrika)(store)(next)(action); @@ -52,7 +52,7 @@ describe('lib/static/modules/middlewares/metrika', () => { it('should create yandex metrika instance after get updated state', () => { const store = mkStore_(); - const action = {type: actionNames.VIEW_INITIAL}; + const action = {type: actionNames.INIT_GUI_REPORT}; metrikaMiddleware(YandexMetrika)(store)(next)(action); @@ -69,7 +69,7 @@ describe('lib/static/modules/middlewares/metrika', () => { } } }); - const action = {type: actionNames.VIEW_INITIAL}; + const action = {type: actionNames.INIT_GUI_REPORT}; metrikaMiddleware(YandexMetrika)(store)(next)(action); diff --git a/test/unit/lib/static/modules/query-params.js b/test/unit/lib/static/modules/query-params.js index 90141307e..0679399f0 100644 --- a/test/unit/lib/static/modules/query-params.js +++ b/test/unit/lib/static/modules/query-params.js @@ -1,5 +1,3 @@ -'use strict'; - import {parseQuery, appendQuery} from 'lib/static/modules/query-params'; import viewModes from 'lib/constants/view-modes'; diff --git a/test/unit/lib/static/modules/reducers/processing.js b/test/unit/lib/static/modules/reducers/processing.js new file mode 100644 index 000000000..53d85f18e --- /dev/null +++ b/test/unit/lib/static/modules/reducers/processing.js @@ -0,0 +1,34 @@ +import reducer from 'lib/static/modules/reducers/processing'; +import actionNames from 'lib/static/modules/action-names'; + +describe('lib/static/modules/reducers/processing', () => { + [ + actionNames.RUN_ALL_TESTS, + actionNames.RUN_FAILED_TESTS, + actionNames.RETRY_SUITE, + actionNames.RETRY_TEST, + actionNames.PROCESS_BEGIN + ].forEach((type) => { + describe(`"${type}" action`, () => { + it('should set processing flag', () => { + const action = {type}; + + const newState = reducer(undefined, action); + + assert.isTrue(newState.processing); + }); + }); + }); + + [actionNames.TESTS_END, actionNames.PROCESS_END].forEach((type) => { + describe(`"${type}" action`, () => { + it('should reset processing flag', () => { + const action = {type}; + + const newState = reducer({processing: true}, action); + + assert.isFalse(newState.processing); + }); + }); + }); +}); diff --git a/test/unit/lib/static/modules/reducers/reporter.js b/test/unit/lib/static/modules/reducers/reporter.js deleted file mode 100644 index d2ced2ca7..000000000 --- a/test/unit/lib/static/modules/reducers/reporter.js +++ /dev/null @@ -1,393 +0,0 @@ -'use strict'; - -import actionNames from 'lib/static/modules/action-names'; -import defaultState from 'lib/static/modules/default-state'; -import {appendQuery, encodeBrowsers} from 'lib/static/modules/query-params'; -import viewModes from 'lib/constants/view-modes'; -import StaticTestsTreeBuilder from 'lib/tests-tree-builder/static'; -import {mkStorage} from '../../../../utils'; - -const {assign} = require('lodash'); -const proxyquire = require('proxyquire'); - -const localStorageWrapper = {}; - -describe('lib/static/modules/reducers/reporter', () => { - const sandbox = sinon.sandbox.create(); - let reducer, baseUrl; - - const mkReducer_ = () => { - return proxyquire('lib/static/modules/reducers/reporter', { - './helpers/local-storage-wrapper': localStorageWrapper - }).default; - }; - - beforeEach(() => { - localStorageWrapper.setItem = sinon.stub(); - localStorageWrapper.getItem = sinon.stub(); - - sandbox.stub(StaticTestsTreeBuilder, 'create') - .returns(Object.create(StaticTestsTreeBuilder.prototype)); - - sandbox.stub(StaticTestsTreeBuilder.prototype, 'build').returns({}); - - baseUrl = 'http://localhost/'; - global.window = { - location: { - href: new URL(baseUrl) - }, - localStorage: mkStorage() - }; - - reducer = mkReducer_(); - }); - - afterEach(() => { - global.window = undefined; - sandbox.restore(); - }); - - describe('regression', () => { - it('shouldn\'t change "Expand" filter when "Show all" or "Show only failed" state changing', function() { - let newState = [ - {type: actionNames.VIEW_EXPAND_RETRIES}, - {type: actionNames.VIEW_SHOW_ALL} - ].reduce(reducer, defaultState); - - assert.equal(newState.view.expand, 'retries'); - - newState = [ - {type: actionNames.VIEW_EXPAND_RETRIES}, - {type: actionNames.VIEW_SHOW_FAILED} - ].reduce(reducer, defaultState); - - assert.equal(newState.view.expand, 'retries'); - }); - }); - - describe('VIEW_INITIAL', () => { - const baseUrl = 'http://localhost/'; - - const _mkInitialState = (state = {}) => { - return { - config: { - errorPatterns: [] - }, - ...state - }; - }; - - describe('"errorPatterns" field in config', () => { - it('should add "regexp" field', () => { - const errorPatterns = [{name: 'err1', pattern: 'pattern1'}]; - const action = { - type: actionNames.VIEW_INITIAL, - payload: _mkInitialState({ - config: {errorPatterns} - }) - }; - - const newState = reducer(defaultState, action); - - assert.deepEqual(newState.config.errorPatterns, [{name: 'err1', pattern: 'pattern1', regexp: /pattern1/}]); - }); - - it('should not modify original object', () => { - const origErrorPatterns = [{name: 'err1', pattern: 'pattern1'}]; - const action = { - type: actionNames.VIEW_INITIAL, - payload: _mkInitialState({ - config: {errorPatterns: origErrorPatterns} - }) - }; - - const newState = reducer(defaultState, action); - - assert.notExists(origErrorPatterns[0].regexp); - assert.notDeepEqual(newState.config.errorPatterns, origErrorPatterns); - }); - }); - - describe('query params', () => { - describe('"browser" option', () => { - it('should set "filteredBrowsers" property to an empty array by default', () => { - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.filteredBrowsers, []); - }); - - it('should set "filteredBrowsers" property to specified browsers', () => { - global.window.location = new URL(`${baseUrl}?browser=firefox&browser=safari`); - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.filteredBrowsers, [ - {id: 'firefox', versions: []}, - {id: 'safari', versions: []} - ]); - }); - - it('should set "filteredBrowsers" property to specified browsers and versions', () => { - global.window.location = new URL(`${baseUrl}?browser=firefox&browser=safari:23,11.2`); - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.filteredBrowsers, [ - {id: 'firefox', versions: []}, - {id: 'safari', versions: ['23', '11.2']} - ]); - }); - - it('should be able to encode and decode browser ids and versions', () => { - const url = appendQuery(baseUrl, { - browser: encodeBrowsers([{id: 'safari:some', versions: ['v:1', 'v,2']}]) - }); - - global.window.location = new URL(url); - - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.filteredBrowsers, [{ - id: 'safari:some', - versions: ['v:1', 'v,2'] - }]); - }); - }); - - describe('"testNameFilter" option', () => { - it('should set "testNameFilter" property to an empty array by default', () => { - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.testNameFilter, ''); - }); - - it('should set "testNameFilter" property to specified value', () => { - global.window.location = new URL(`${baseUrl}?testNameFilter=sometest`); - - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.testNameFilter, 'sometest'); - }); - }); - - describe('"strictMatchFilter" option', () => { - it('should set "strictMatchFilter" property to an empty array by default', () => { - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.strictMatchFilter, false); - }); - - it('should set "strictMatchFilter" property to specified value', () => { - global.window.location = new URL(`${baseUrl}?strictMatchFilter=true`); - - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.strictMatchFilter, true); - }); - }); - - describe('"retryIndex" option', () => { - it('should set "retryIndex" property to an empty array by default', () => { - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert(!('retryIndex' in newState.view)); - }); - - it('should set "retryIndex" property to string when not a number specified', () => { - global.window.location = new URL(`${baseUrl}?retryIndex=1abc`); - - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.retryIndex, '1abc'); - }); - - it('should set "retryIndex" property to specified number', () => { - global.window.location = new URL(`${baseUrl}?retryIndex=10`); - - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.retryIndex, 10); - }); - }); - - describe('"viewMode" option', () => { - it('should set "viewMode" to "all" by default', () => { - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.viewMode, viewModes.ALL); - }); - - it('should set "viewMode" property to specified value', () => { - global.window.location = new URL(`${baseUrl}?viewMode=failed`); - - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.viewMode, viewModes.FAILED); - }); - }); - - describe('"expand" option', () => { - it('should set "expand" to "errors" by default', () => { - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.expand, 'errors'); - }); - - it('should set "expand" property to specified value', () => { - global.window.location = new URL(`${baseUrl}?expand=all`); - - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.expand, 'all'); - }); - }); - - describe('"groupByError" option', () => { - it('should set "groupByError" to false by default', () => { - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.groupByError, false); - }); - - it('should set "groupByError" property to specified value', () => { - global.window.location = new URL(`${baseUrl}?groupByError=true`); - - const action = {type: actionNames.VIEW_INITIAL, payload: _mkInitialState()}; - - const newState = reducer(undefined, action); - - assert.deepStrictEqual(newState.view.groupByError, true); - }); - }); - }); - }); - - describe('VIEW_SHOW_ALL', () => { - it('should change "viewMode" field on "all" value', () => { - const action = {type: actionNames.VIEW_SHOW_ALL}; - - const newState = reducer(defaultState, action); - - assert.equal(newState.view.viewMode, viewModes.ALL); - }); - }); - - describe('VIEW_SHOW_FAILED', () => { - it('should change "viewMode" field on "failed" value', () => { - const action = {type: actionNames.VIEW_SHOW_FAILED}; - - const newState = reducer(defaultState, action); - - assert.equal(newState.view.viewMode, viewModes.FAILED); - }); - }); - - describe('PROCESS_BEGIN', () => { - it('should set processing flag', () => { - const action = {type: actionNames.PROCESS_BEGIN}; - - const newState = reducer(defaultState, action); - - assert.isTrue(newState.processing); - }); - }); - - describe('PROCESS_END', () => { - it('should reset processing flag', () => { - const action = {type: actionNames.PROCESS_END}; - - const newState = reducer(assign(defaultState, {processing: true}), action); - - assert.isFalse(newState.processing); - }); - }); - - describe(`${actionNames.FETCH_DB}`, () => { - it('should build correct tree', () => { - const suitesFromDb = ['rows-with-suites']; - const suitesTree = [{suitePath: ['some-path']}]; - - StaticTestsTreeBuilder.prototype.build - .withArgs(suitesFromDb) - .returns({tree: {suites: suitesTree}, stats: {}}); - - const db = { - exec: sinon.stub().onFirstCall().returns([{values: suitesFromDb}]) - }; - const action = { - type: actionNames.FETCH_DB, - payload: { - db, - fetchDbDetails: [{url: 'stub'}] - } - }; - - const newState = reducer(defaultState, action); - - assert.deepEqual(newState.suites, suitesTree); - }); - }); - - describe('storing state in browser storage', () => { - it('should be done for actions that start with VIEW prefix', () => { - const action = {type: 'VIEW_FOO_ACTION'}; - - reducer(defaultState, action); - - assert.calledOnce(localStorageWrapper.setItem); - }); - - it('should be skipped for all actions that do not start with VIEW prefix', () => { - const action = {type: 'BAR_ACTION'}; - - reducer(defaultState, action); - - assert.notCalled(localStorageWrapper.setItem); - }); - - it('should include all view params of state except for inputs', () => { - const action = {type: 'VIEW_FOO_ACTION'}; - - reducer(defaultState, action); - - assert.calledOnceWith(localStorageWrapper.setItem, 'view', { - expand: 'errors', - groupByError: false, - scaleImages: false, - showOnlyDiff: false, - showSkipped: false, - strictMatchFilter: false, - viewMode: viewModes.ALL - }); - }); - }); -}); diff --git a/test/unit/lib/static/modules/reducers/view.js b/test/unit/lib/static/modules/reducers/view.js new file mode 100644 index 000000000..de8515cb3 --- /dev/null +++ b/test/unit/lib/static/modules/reducers/view.js @@ -0,0 +1,241 @@ +import reducer from 'lib/static/modules/reducers/view'; +import actionNames from 'lib/static/modules/action-names'; +import defaultState from 'lib/static/modules/default-state'; +import {appendQuery, encodeBrowsers} from 'lib/static/modules/query-params'; +import viewModes from 'lib/constants/view-modes'; +import {mkStorage} from '../../../../utils'; + +describe('lib/static/modules/reducers/view', () => { + let baseUrl; + + beforeEach(() => { + baseUrl = 'http://localhost/'; + global.window = { + location: { + href: new URL(baseUrl) + }, + localStorage: mkStorage() + }; + }); + + afterEach(() => { + global.window = undefined; + }); + + [actionNames.INIT_GUI_REPORT, actionNames.INIT_STATIC_REPORT].forEach((type) => { + describe(`"${type}" action`, () => { + const baseUrl = 'http://localhost/'; + + const _mkInitialState = (state = {}) => { + return { + config: { + errorPatterns: [] + }, + ...state + }; + }; + + describe('query params', () => { + describe('"browser" option', () => { + it('should set "filteredBrowsers" property to an empty array by default', () => { + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.filteredBrowsers, []); + }); + + it('should set "filteredBrowsers" property to specified browsers', () => { + global.window.location = new URL(`${baseUrl}?browser=firefox&browser=safari`); + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.filteredBrowsers, [ + {id: 'firefox', versions: []}, + {id: 'safari', versions: []} + ]); + }); + + it('should set "filteredBrowsers" property to specified browsers and versions', () => { + global.window.location = new URL(`${baseUrl}?browser=firefox&browser=safari:23,11.2`); + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.filteredBrowsers, [ + {id: 'firefox', versions: []}, + {id: 'safari', versions: ['23', '11.2']} + ]); + }); + + it('should be able to encode and decode browser ids and versions', () => { + const url = appendQuery(baseUrl, { + browser: encodeBrowsers([{id: 'safari:some', versions: ['v:1', 'v,2']}]) + }); + + global.window.location = new URL(url); + + const action = {type, payload: _mkInitialState()}; + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.filteredBrowsers, [{ + id: 'safari:some', + versions: ['v:1', 'v,2'] + }]); + }); + }); + + describe('"testNameFilter" option', () => { + it('should set "testNameFilter" property to an empty array by default', () => { + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.testNameFilter, ''); + }); + + it('should set "testNameFilter" property to specified value', () => { + global.window.location = new URL(`${baseUrl}?testNameFilter=sometest`); + + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.testNameFilter, 'sometest'); + }); + }); + + describe('"strictMatchFilter" option', () => { + it('should set "strictMatchFilter" property to an empty array by default', () => { + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.strictMatchFilter, false); + }); + + it('should set "strictMatchFilter" property to specified value', () => { + global.window.location = new URL(`${baseUrl}?strictMatchFilter=true`); + + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.strictMatchFilter, true); + }); + }); + + describe('"retryIndex" option', () => { + it('should set "retryIndex" property to an empty array by default', () => { + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert(!('retryIndex' in newState.view)); + }); + + it('should set "retryIndex" property to string when not a number specified', () => { + global.window.location = new URL(`${baseUrl}?retryIndex=1abc`); + + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.retryIndex, '1abc'); + }); + + it('should set "retryIndex" property to specified number', () => { + global.window.location = new URL(`${baseUrl}?retryIndex=10`); + + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.retryIndex, 10); + }); + }); + + describe('"viewMode" option', () => { + it('should set "viewMode" to "all" by default', () => { + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.viewMode, viewModes.ALL); + }); + + it('should set "viewMode" property to specified value', () => { + global.window.location = new URL(`${baseUrl}?viewMode=failed`); + + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.viewMode, viewModes.FAILED); + }); + }); + + describe('"expand" option', () => { + it('should set "expand" to "errors" by default', () => { + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.expand, 'errors'); + }); + + it('should set "expand" property to specified value', () => { + global.window.location = new URL(`${baseUrl}?expand=all`); + + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.expand, 'all'); + }); + }); + + describe('"groupByError" option', () => { + it('should set "groupByError" to false by default', () => { + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.groupByError, false); + }); + + it('should set "groupByError" property to specified value', () => { + global.window.location = new URL(`${baseUrl}?groupByError=true`); + + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(defaultState, action); + + assert.deepStrictEqual(newState.view.groupByError, true); + }); + }); + }); + }); + }); + + describe(`"${actionNames.VIEW_SHOW_ALL}" action`, () => { + it('should change "viewMode" field on "all" value', () => { + const action = {type: actionNames.VIEW_SHOW_ALL}; + + const newState = reducer(defaultState, action); + + assert.equal(newState.view.viewMode, viewModes.ALL); + }); + }); + + describe(`"${actionNames.VIEW_SHOW_FAILED}" action`, () => { + it('should change "viewMode" field on "failed" value', () => { + const action = {type: actionNames.VIEW_SHOW_FAILED}; + + const newState = reducer(defaultState, action); + + assert.equal(newState.view.viewMode, viewModes.FAILED); + }); + }); +}); diff --git a/test/unit/lib/static/modules/selectors/stats.js b/test/unit/lib/static/modules/selectors/stats.js new file mode 100644 index 000000000..d3703df68 --- /dev/null +++ b/test/unit/lib/static/modules/selectors/stats.js @@ -0,0 +1,82 @@ +import {getStatsFilteredByBrowsers} from 'lib/static/modules/selectors/stats'; + +describe('stats selectors', () => { + describe('getStatsFilteredByBrowsers', () => { + const stats = { + all: { + total: 40, + passed: 20, + failed: 10, + skipped: 10, + retries: 30 + }, + perBrowser: { + bro1: { + ver1: {failed: 1, passed: 1}, + ver2: {failed: 1, passed: 1} + }, + bro2: { + ver1: {failed: 1, passed: 1}, + ver2: {failed: 1, passed: 1} + }, + bro3: { + ver1: {failed: 1, passed: 1}, + ver2: {failed: 1, passed: 1}, + ver3: {failed: 1, passed: 1} + } + } + }; + + it('should return correct statistics when it is not filtered', () => { + const view = {filteredBrowsers: []}; + + const filteredStats = getStatsFilteredByBrowsers({stats, view}); + + assert.deepEqual(filteredStats, { + total: 40, + passed: 20, + failed: 10, + skipped: 10, + retries: 30 + }); + }); + + it('should return correct statistics for one filtered browser', () => { + const view = {filteredBrowsers: [{id: 'bro1'}]}; + + const filteredStats = getStatsFilteredByBrowsers({stats, view}); + + assert.deepEqual(filteredStats, { + passed: 2, + failed: 2 + }); + }); + + it('should return correct statistics for several filtered browsers', () => { + const view = {filteredBrowsers: [{id: 'bro1'}, {id: 'bro2'}]}; + + const filteredStats = getStatsFilteredByBrowsers({stats, view}); + + assert.deepEqual(filteredStats, { + passed: 4, + failed: 4 + }); + }); + + it('should return correct statistics corresponding to versions', () => { + const view = { + filteredBrowsers: [ + {id: 'bro1', versions: ['ver1', 'ver2']}, + {id: 'bro2', versions: ['ver1']} + ] + }; + + const filteredStats = getStatsFilteredByBrowsers({stats, view}); + + assert.deepEqual(filteredStats, { + passed: 3, + failed: 3 + }); + }); + }); +}); diff --git a/test/unit/lib/static/modules/selectors/tree.js b/test/unit/lib/static/modules/selectors/tree.js new file mode 100644 index 000000000..edc5e2a69 --- /dev/null +++ b/test/unit/lib/static/modules/selectors/tree.js @@ -0,0 +1,272 @@ +import {defaults} from 'lodash'; +import {SUCCESS, ERROR} from 'lib/constants/test-statuses'; +import {mkShouldSuiteBeShown, mkShouldBrowserBeShown} from 'lib/static/modules/selectors/tree'; +import viewModes from 'lib/constants/view-modes'; + +describe('tree selectors', () => { + const mkSuite = (opts) => { + const result = defaults(opts, { + id: 'default-suite-id', + parentId: null, + name: 'default-name', + status: SUCCESS + }); + + return {[result.id]: result}; + }; + + const mkBrowser = (opts) => { + const browser = defaults(opts, { + id: 'default-bro-id', + name: 'default-bro', + parentId: 'default-test-id', + resultIds: [], + versions: [] + }); + + return {[browser.id]: browser}; + }; + + const mkResult = (opts) => { + const result = defaults(opts, { + id: 'default-result-id', + parentId: 'default-bro-id', + status: SUCCESS, + imageIds: [] + }); + + return {[result.id]: result}; + }; + + const mkStateTree = ({suitesById, browsersById = {}, resultsById = {}, imagesById = {}} = {}) => { + return { + suites: {byId: suitesById}, + browsers: {byId: browsersById}, + results: {byId: resultsById}, + images: {byId: imagesById} + }; + }; + + const mkStateView = (opts = {}) => { + return defaults(opts, { + viewMode: viewModes.ALL, + testNameFilter: '', + strictMatchFilter: false, + filteredBrowsers: [] + }); + }; + + const mkState = ({tree = mkStateTree(), view = mkStateView()} = {}) => ({tree, view}); + + describe('"mkShouldSuiteBeShown" factory', () => { + describe('viewMode', () => { + [ + {name: '"all" for success suite', status: SUCCESS, viewMode: viewModes.ALL}, + {name: '"all" for error suite', status: ERROR, viewMode: viewModes.ALL}, + {name: '"failed" for error suite', status: ERROR, viewMode: viewModes.FAILED} + ].forEach(({name, status, viewMode}) => { + it(`should be true if viewMode is ${name}`, () => { + const suitesById = { + ...mkSuite({id: 's1', status, suiteIds: ['s2']}), + ...mkSuite({id: 's2', status, browserIds: ['b1']}) + }; + const browsersById = mkBrowser({id: 'b1'}); + + const tree = mkStateTree({suitesById, browsersById}); + const state = mkState({tree, view: {viewMode}}); + + assert.isTrue(mkShouldSuiteBeShown()(state, {suiteId: 's1'})); + }); + }); + + it('should be false if viewMode is "failed" for success suite', () => { + const suitesById = { + ...mkSuite({id: 's1', status: SUCCESS, suiteIds: ['s2']}), + ...mkSuite({id: 's2', status: SUCCESS, browserIds: ['b1']}) + }; + const browsersById = mkBrowser({id: 'b1'}); + + const tree = mkStateTree({suitesById, browsersById}); + const state = mkState({tree, view: {viewMode: viewModes.FAILED}}); + + assert.isFalse(mkShouldSuiteBeShown()(state, {suiteId: 's1'})); + }); + }); + + describe('testNameFilter', () => { + [ + {name: 'top-level title matches', testNameFilter: 's1'}, + {name: 'bottom-level title matches', testNameFilter: 's2'}, + {name: 'if full title matches', testNameFilter: 's1 s2'} + ].forEach(({name, testNameFilter}) => { + it(`should be true if ${name}`, () => { + const suitesById = { + ...mkSuite({id: 's1', status: SUCCESS, suiteIds: ['s2']}), + ...mkSuite({id: 's2', status: SUCCESS, browserIds: ['b1']}) + }; + const browsersById = mkBrowser({id: 'b1', parentId: 's1 s2'}); + + const tree = mkStateTree({suitesById, browsersById}); + const view = mkStateView({testNameFilter}); + const state = mkState({tree, view}); + + assert.isTrue(mkShouldSuiteBeShown()(state, {suiteId: 's1'})); + }); + }); + + [ + {name: 'no matches found', testNameFilter: 'not_found'}, + {name: 'only part of top-level title matches', testNameFilter: 's1 s3'}, + {name: 'only part of bottom-level title matches', testNameFilter: 's3 s2'} + ].forEach(({name, testNameFilter}) => { + it(`should be false if ${name}`, () => { + const suitesById = { + ...mkSuite({id: 's1', status: SUCCESS, suiteIds: ['s2']}), + ...mkSuite({id: 's2', status: SUCCESS, browserIds: ['b1']}) + }; + const browsersById = mkBrowser({id: 'b1', parentId: 's1 s2'}); + + const tree = mkStateTree({suitesById, browsersById}); + const view = mkStateView({testNameFilter}); + const state = mkState({tree, view}); + + assert.isFalse(mkShouldSuiteBeShown()(state, {suiteId: 's1'})); + }); + }); + }); + + describe('strictMatchFilter', () => { + [ + {name: 'only top-level title matches', testNameFilter: 's1 s3'}, + {name: 'only bottom-level title matches', testNameFilter: 's3 s1'}, + {name: 'not matches found', testNameFilter: 'not_found'} + ].forEach(({name, testNameFilter}) => { + it(`should be false if ${name}`, () => { + const suitesById = { + ...mkSuite({id: 's1', status: SUCCESS, suiteIds: ['s2']}), + ...mkSuite({id: 's2', status: SUCCESS, browserIds: ['b1']}) + }; + const browsersById = mkBrowser({id: 'b1', parentId: 's1 s2'}); + + const tree = mkStateTree({suitesById, browsersById}); + const view = mkStateView({testNameFilter, strictMatchFilter: true}); + const state = mkState({tree, view}); + + assert.isFalse(mkShouldSuiteBeShown()(state, {suiteId: 's1'})); + }); + }); + + it('should be true if full title matches', () => { + const suitesById = { + ...mkSuite({id: 's1', status: SUCCESS, suiteIds: ['s2']}), + ...mkSuite({id: 's2', status: SUCCESS, browserIds: ['b1']}) + }; + const browsersById = mkBrowser({id: 'b1', parentId: 's1 s2'}); + + const tree = mkStateTree({suitesById, browsersById}); + const view = mkStateView({testNameFilter: 's1 s2', strictMatchFilter: true}); + const state = mkState({tree, view}); + + assert.isTrue(mkShouldSuiteBeShown()(state, {suiteId: 's1'})); + }); + }); + + describe('errorGroupBrowserIds', () => { + it('should be false if browser id not match', () => { + const suitesById = { + ...mkSuite({id: 's1', status: SUCCESS, suiteIds: ['s2']}), + ...mkSuite({id: 's2', status: SUCCESS, browserIds: ['b1']}) + }; + const browsersById = mkBrowser({id: 'b1', parentId: 's1 s2'}); + + const tree = mkStateTree({suitesById, browsersById}); + const state = mkState({tree}); + + assert.isFalse(mkShouldSuiteBeShown()(state, {suiteId: 's1', errorGroupBrowserIds: ['not_found']})); + }); + + it('should be true if browser id is match', () => { + const suitesById = { + ...mkSuite({id: 's1', status: SUCCESS, suiteIds: ['s2']}), + ...mkSuite({id: 's2', status: SUCCESS, browserIds: ['b1']}) + }; + const browsersById = mkBrowser({id: 'b1', parentId: 's1 s2'}); + + const tree = mkStateTree({suitesById, browsersById}); + const state = mkState({tree}); + + assert.isTrue(mkShouldSuiteBeShown()(state, {suiteId: 's1', errorGroupBrowserIds: ['b1']})); + }); + }); + + describe('filteredBrowsers', () => { + [ + {name: 'browser name is equal', filteredBrowsers: [{id: 'yabro'}]}, + {name: 'browser name and versions are equal', filteredBrowsers: [{id: 'yabro', versions: ['1']}]} + ].forEach(({name, filteredBrowsers}) => { + it(`should be true if ${name}`, () => { + const suitesById = { + ...mkSuite({id: 's1', status: SUCCESS, suiteIds: ['s2']}), + ...mkSuite({id: 's2', status: SUCCESS, browserIds: ['b1']}) + }; + const browsersById = mkBrowser({id: 'b1', name: 'yabro', versions: ['1'], parentId: 's1 s2'}); + + const tree = mkStateTree({suitesById, browsersById}); + const view = mkStateView({filteredBrowsers}); + const state = mkState({tree, view}); + + assert.isTrue(mkShouldSuiteBeShown()(state, {suiteId: 's1'})); + }); + }); + + [ + {name: 'browser name is not equal', filteredBrowsers: [{id: 'some-bro'}]}, + {name: 'browser name is equal but versions arent', filteredBrowsers: [{id: 'yabro', versions: ['2']}]} + ].forEach(({name, filteredBrowsers}) => { + it(`should be true if ${name}`, () => { + const suitesById = { + ...mkSuite({id: 's1', status: SUCCESS, suiteIds: ['s2']}), + ...mkSuite({id: 's2', status: SUCCESS, browserIds: ['b1']}) + }; + const browsersById = mkBrowser({id: 'b1', name: 'yabro', versions: ['1'], parentId: 's1 s2'}); + + const tree = mkStateTree({suitesById, browsersById}); + const view = mkStateView({filteredBrowsers}); + const state = mkState({tree, view}); + + assert.isFalse(mkShouldSuiteBeShown()(state, {suiteId: 's1'})); + }); + }); + }); + }); + + describe('"mkShouldBrowserBeShown" factory', () => { + describe('viewMode', () => { + [ + {name: '"all" for success browser', status: SUCCESS, viewMode: viewModes.ALL}, + {name: '"all" for error browser', status: ERROR, viewMode: viewModes.ALL}, + {name: '"failed" for error browser', status: ERROR, viewMode: viewModes.FAILED} + ].forEach(({name, status, viewMode}) => { + it(`should be true if viewMode is ${name}`, () => { + const browsersById = mkBrowser({id: 'b1'}); + const resultsById = mkResult({id: 'r1', status}); + + const tree = mkStateTree({browsersById, resultsById}); + const state = mkState({tree, view: {viewMode}}); + + assert.isTrue(mkShouldBrowserBeShown()(state, {suiteId: 's1', result: resultsById['r1']})); + }); + }); + + it('should be false if viewMode is "failed" for success browser', () => { + const browsersById = mkBrowser({id: 'b1', resultIds: ['r1']}); + const resultsById = mkResult({id: 'r1', status: SUCCESS}); + + const tree = mkStateTree({browsersById, resultsById}); + const state = mkState({tree, view: {viewMode: viewModes.FAILED}}); + + assert.isFalse(mkShouldBrowserBeShown()(state, {browserId: 's1', result: resultsById['r1']})); + }); + }); + }); +}); diff --git a/test/unit/lib/static/modules/utils.js b/test/unit/lib/static/modules/utils.js index c9600fa53..eb430c1c1 100644 --- a/test/unit/lib/static/modules/utils.js +++ b/test/unit/lib/static/modules/utils.js @@ -1,134 +1,10 @@ 'use strict'; const utils = require('lib/static/modules/utils'); -const { - IDLE, - FAIL, - ERROR, - SKIPPED, - SUCCESS -} = require('lib/constants/test-statuses'); -const { - NO_REF_IMAGE_ERROR -} = require('lib/constants/errors').getCommonErrors(); -const { - mkSuite, - mkState, - mkBrowserResult -} = require('../../../utils'); -const viewModes = require('lib/constants/view-modes'); +const {IDLE, FAIL, ERROR, SKIPPED, SUCCESS} = require('lib/constants/test-statuses'); +const {NO_REF_IMAGE_ERROR} = require('lib/constants/errors').getCommonErrors(); describe('static/modules/utils', () => { - describe('hasFails', () => { - describe('should return true for node if', () => { - const mkNode_ = ({imageStatus = SUCCESS, status = SUCCESS}) => { - return { - result: { - imagesInfo: [{status: SUCCESS}, {status: SUCCESS}, {status: imageStatus}], - status - } - }; - }; - - it('at least one image is with failed status', () => { - const node = mkNode_({imageStatus: FAIL}); - - assert.isTrue(utils.hasFails(node)); - }); - - it('at least one image is with errored status', () => { - const node = mkNode_({imageStatus: ERROR}); - - assert.isTrue(utils.hasFails(node)); - }); - - it('no images with failed or errored statuses but test is failed', () => { - const node = mkNode_({status: FAIL}); - - assert.isTrue(utils.hasFails(node)); - }); - - it('no images with failed or errored statuses but test is errored', () => { - const node = mkNode_({status: ERROR}); - - assert.isTrue(utils.hasFails(node)); - }); - }); - - describe('should return true for node with good result (no fail or error status) if', () => { - const mkNode_ = (key, {imageStatus = SUCCESS, status = SUCCESS}) => { - const goodResult = { - result: { - imagesInfo: [{status: SUCCESS}, {status: SUCCESS}, {status: SUCCESS}], - status: SUCCESS - } - }; - - return { - ...goodResult, - [key]: [ - {...goodResult}, - { - result: { - imagesInfo: [{status: SUCCESS}, {status: SUCCESS}, {status: imageStatus}], - status - } - }, - {...goodResult} - ] - }; - }; - - it('at least one image in browsers of node is with failed status', () => { - const node = mkNode_('browsers', {imageStatus: FAIL}); - - assert.isTrue(utils.hasFails(node)); - }); - - it('at least one image in children of node is with failed status', () => { - const node = mkNode_('children', {imageStatus: FAIL}); - - assert.isTrue(utils.hasFails(node)); - }); - - it('at least one image in browsers is with errored status', () => { - const node = mkNode_('browsers', {imageStatus: ERROR}); - - assert.isTrue(utils.hasFails(node)); - }); - - it('at least one image in children is with errored status', () => { - const node = mkNode_('children', {imageStatus: ERROR}); - - assert.isTrue(utils.hasFails(node)); - }); - - it('in browsers no images with failed or errored statuses but test is failed', () => { - const node = mkNode_('browsers', {status: FAIL}); - - assert.isTrue(utils.hasFails(node)); - }); - - it('in children no images with failed or errored statuses but test is failed', () => { - const node = mkNode_('children', {status: FAIL}); - - assert.isTrue(utils.hasFails(node)); - }); - - it('in browsers no images with failed or errored statuses but test is errored', () => { - const node = mkNode_('browsers', {status: ERROR}); - - assert.isTrue(utils.hasFails(node)); - }); - - it('in children no images with failed or errored statuses but test is errored', () => { - const node = mkNode_('children', {status: ERROR}); - - assert.isTrue(utils.hasFails(node)); - }); - }); - }); - describe('isSuiteIdle', () => { it('should return true for idle test', () => { assert.isTrue(utils.isSuiteIdle({status: IDLE})); @@ -213,353 +89,6 @@ describe('static/modules/utils', () => { }); }); - describe('filterSuites', () => { - let suites; - - beforeEach(() => { - suites = [mkSuite({ - browsers: [ - mkBrowserResult({name: 'first-bro'}), - mkBrowserResult({name: 'third-bro'}) - ], - children: [ - mkState({ - browsers: [mkBrowserResult({name: 'first-bro'})] - }), - mkState({ - browsers: [mkBrowserResult({name: 'second-bro'})] - }) - ] - })]; - }); - - it('should return suites as is if no browsers to filter', () => { - assert.deepEqual(suites, utils.filterSuites(suites)); - }); - - it('should return empty suites if no suites given', () => { - assert.deepEqual([], utils.filterSuites([], ['some-bro'])); - }); - - it('should filter suites by given browsers', () => { - const filteredSuites = [mkSuite({ - browsers: [mkBrowserResult({name: 'first-bro'})], - children: [ - mkState({ - browsers: [mkBrowserResult({name: 'first-bro'})] - }) - ] - })]; - - assert.deepEqual(filteredSuites, utils.filterSuites(suites, ['first-bro'])); - }); - }); - - 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.shouldSuiteBeShown({suite, testNameFilter: 'Some suite'})); - }); - - 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.shouldSuiteBeShown({suite, testNameFilter: 'test two'})); - }); - - 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.shouldSuiteBeShown({suite, testNameFilter: 'Some suite test two'}) - ); - }); - - it('should be false if only part of only bottom-level title matches', () => { - assert.isFalse( - utils.shouldSuiteBeShown({suite, testNameFilter: 'Another suite test one'}) - ); - }); - }); - - describe('strictMatchFilter', () => { - const suite = mkSuite({ - suitePath: ['Some suite'], - children: [ - mkState({ - suitePath: ['Some suite', 'test one'] - }) - ] - }); - - it('should be false if top-level title matches but filter is strict', () => { - assert.isFalse(utils.shouldSuiteBeShown({suite, strictMatchFilter: true, testNameFilter: 'Some suite'})); - }); - - it('should be false if bottom-level title matches but filter is strict', () => { - assert.isFalse(utils.shouldSuiteBeShown({suite, strictMatchFilter: true, testNameFilter: 'test one'})); - }); - - it('should be false if no matches found', () => { - assert.isFalse(utils.shouldSuiteBeShown({suite, strictMatchFilter: true, testNameFilter: 'test two'})); - }); - - it('should be true if full title matches', () => { - assert.isTrue( - utils.shouldSuiteBeShown({suite, strictMatchFilter: true, testNameFilter: 'Some suite test one'}) - ); - }); - - it('should be false if only part of only top-level title matches', () => { - assert.isFalse( - utils.shouldSuiteBeShown({suite, strictMatchFilter: true, testNameFilter: 'Some suite test two'}) - ); - }); - - it('should be false if only part of only bottom-level title matches', () => { - assert.isFalse( - utils.shouldSuiteBeShown({suite, strictMatchFilter: true, testNameFilter: 'Another suite test one'}) - ); - }); - }); - - describe('errorGroupTests', () => { - const suite = mkSuite({ - suitePath: ['Some suite'], - children: [ - mkState({ - suitePath: ['Some suite', 'test one'] - }) - ] - }); - - 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 true if full title matches', () => { - assert.isTrue( - utils.shouldSuiteBeShown({suite, errorGroupTests: {'Some suite test one': []}}) - ); - }); - }); - - describe('filteredBrowsers', () => { - const defaultSuite = mkSuite({ - children: [ - mkState({ - browsers: [mkBrowserResult({name: 'bro1'})] - }) - ] - }); - - it('should be true if browser id is equal', () => { - assert.isTrue(utils.shouldSuiteBeShown({suite: defaultSuite, filteredBrowsers: [{id: 'bro1'}]})); - }); - - it('should be false if browser id is not a strict match', () => { - assert.isFalse(utils.shouldSuiteBeShown({suite: defaultSuite, filteredBrowsers: [{id: 'bro'}]})); - }); - - it('should be false if browser id is not equal', () => { - assert.isFalse(utils.shouldSuiteBeShown({suite: defaultSuite, filteredBrowsers: [{id: 'non-existing-id'}]})); - }); - - it('should be true if browser id is equal when suite contains children and browsers', () => { - const suite = mkSuite({ - children: [ - mkSuite({ - browsers: [mkBrowserResult({name: 'bro1'})], - children: [ - mkState({ - browsers: [mkBrowserResult({name: 'bro2'})] - }) - ] - }) - ] - }); - - assert.isTrue(utils.shouldSuiteBeShown({suite, filteredBrowsers: [{id: 'bro1'}]})); - assert.isTrue(utils.shouldSuiteBeShown({suite, filteredBrowsers: [{id: 'bro2'}]})); - }); - - it('should be true if browser id is equal and there is required version', () => { - const suite = mkSuite({ - children: [ - mkSuite({ - browsers: [mkBrowserResult({name: 'bro1', browserVersion: '1.1'})] - }) - ] - }); - - assert.isTrue(utils.shouldSuiteBeShown({suite, filteredBrowsers: [{id: 'bro1', versions: ['1.1']}]})); - }); - - it('should be false if browser id is equal but there is no required version', () => { - const suite = mkSuite({ - children: [ - mkSuite({ - browsers: [mkBrowserResult({name: 'bro1', browserVersion: '1.1'})] - }) - ] - }); - - assert.isFalse(utils.shouldSuiteBeShown({suite, filteredBrowsers: [{id: 'bro1', versions: ['1.2', '1.3']}]})); - }); - }); - - describe('viewMode', () => { - const successSuite = mkSuite({ - status: 'success', - children: [ - mkState({ - browsers: [mkBrowserResult({name: 'first-bro'})] - }) - ] - }); - const errorSuite = mkSuite({ - status: 'error', - children: [ - mkState({ - status: 'error', - browsers: [mkBrowserResult({name: 'second-bro', result: {status: 'error'}})] - }) - ] - }); - - it('should be true if viewMode is "all" for success suite', () => { - assert.isTrue(utils.shouldSuiteBeShown({suite: successSuite, viewMode: viewModes.ALL})); - }); - - it('should be true if viewMode is "all" for error suite', () => { - assert.isTrue(utils.shouldSuiteBeShown({suite: errorSuite, viewMode: viewModes.ALL})); - }); - - it('should be false if viewMode is "failed" for success suite', () => { - assert.isFalse(utils.shouldSuiteBeShown({suite: successSuite, viewMode: viewModes.FAILED})); - }); - - it('should be true if viewMode is "failed" for error suite', () => { - assert.isTrue(utils.shouldSuiteBeShown({suite: errorSuite, viewMode: viewModes.FAILED})); - }); - }); - }); - - describe('shouldBrowserBeShown', () => { - describe('viewMode', () => { - const successBrowser = mkBrowserResult({name: 'first-bro'}); - const errorBrowser = mkBrowserResult({name: 'second-bro', result: {status: 'error'}}); - - it('should be true if viewMode is "all" for success browser', () => { - assert.isTrue(utils.shouldBrowserBeShown({browser: successBrowser, viewMode: viewModes.ALL})); - }); - - it('should be true if viewMode is "all" for error browser', () => { - assert.isTrue(utils.shouldBrowserBeShown({browser: errorBrowser, viewMode: viewModes.ALL})); - }); - - it('should be false if viewMode is "failed" for success browser', () => { - assert.isFalse(utils.shouldBrowserBeShown({browser: successBrowser, viewMode: viewModes.FAILED})); - }); - - it('should be true if viewMode is "failed" for error browser', () => { - assert.isTrue(utils.shouldBrowserBeShown({browser: errorBrowser, viewMode: viewModes.FAILED})); - }); - }); - }); - - describe('getStats', () => { - const inputStats = { - all: { - total: 40, - passed: 20, - failed: 10, - skipped: 10, - retries: 30 - }, - perBrowser: { - bro1: { - ver1: {failed: 1, passed: 1}, - ver2: {failed: 1, passed: 1} - }, - bro2: { - ver1: {failed: 1, passed: 1}, - ver2: {failed: 1, passed: 1} - }, - bro3: { - ver1: {failed: 1, passed: 1}, - ver2: {failed: 1, passed: 1}, - ver3: {failed: 1, passed: 1} - } - } - }; - - it('should return correct statistics when it is not filtered', () => { - const stats = utils.getStats(inputStats, []); - - assert.deepEqual(stats, { - total: 40, - passed: 20, - failed: 10, - skipped: 10, - retries: 30 - }); - }); - - it('should return correct statistics for one filtered browser', () => { - const stats = utils.getStats(inputStats, [{id: 'bro1'}]); - - assert.deepEqual(stats, { - passed: 2, - failed: 2 - }); - }); - - it('should return correct statistics for several filtered browsers', () => { - const stats = utils.getStats(inputStats, [{id: 'bro1'}, {id: 'bro2'}]); - - assert.deepEqual(stats, { - passed: 4, - failed: 4 - }); - }); - - it('should return correct statistics corresponding to versions', () => { - const stats = utils.getStats(inputStats, [ - {id: 'bro1', versions: ['ver1', 'ver2']}, - {id: 'bro2', versions: ['ver1']} - ]); - - assert.deepEqual(stats, { - passed: 3, - failed: 3 - }); - }); - }); - describe('getHttpErrorMessage', () => { it('should return response error', () => { const response = {status: '500', data: 'some-response-error'}; @@ -578,18 +107,4 @@ describe('static/modules/utils', () => { assert.equal(utils.getHttpErrorMessage({...error, response}), '(500) some-response-error'); }); }); - - describe('hasFailedRetries', () => { - [ - {expected: undefined, retries: []}, - {expected: undefined, retries: [{status: 'success'}]}, - {expected: undefined, retries: [null]}, - {expected: true, retries: [{status: 'success'}, {status: 'fail'}]}, - {expected: true, retries: [{status: 'success'}, {status: 'error'}, null]} - ].forEach(({expected, retries}) => { - it(`should return ${expected} for ${JSON.stringify(retries)}`, () => { - assert.strictEqual(utils.hasFailedRetries({retries}), expected); - }); - }); - }); }); diff --git a/test/unit/lib/tests-tree-builder/base.js b/test/unit/lib/tests-tree-builder/base.js index 1a95f1f28..235452f1f 100644 --- a/test/unit/lib/tests-tree-builder/base.js +++ b/test/unit/lib/tests-tree-builder/base.js @@ -3,13 +3,14 @@ const _ = require('lodash'); const proxyquire = require('proxyquire'); const {FAIL, ERROR, SUCCESS} = require('lib/constants/test-statuses'); +const {versions: browserVersions} = require('lib/constants/browser'); describe('ResultsTreeBuilder', () => { const sandbox = sinon.sandbox.create(); let ResultsTreeBuilder, builder, determineStatus; const mkTestResult_ = (result) => { - return _.defaults(result, {imagesInfo: []}); + return _.defaults(result, {imagesInfo: [], metaInfo: {}}); }; const mkFormattedResult_ = (result) => { @@ -130,11 +131,26 @@ describe('ResultsTreeBuilder', () => { id: 's1 b1', name: 'b1', parentId: 's1', - resultIds: ['s1 b1 0'] + resultIds: ['s1 b1 0'], + versions: [browserVersions.UNKNOWN] } ); }); + it('should collect all browser versions from results in browser', () => { + const result1 = mkTestResult_({metaInfo: {browserVersion: '1'}}); + const result2 = mkTestResult_({metaInfo: {browserVersion: '2'}}); + + builder.addTestResult(result1, mkFormattedResult_({ + testPath: ['s1'], browserId: 'b1', attempt: 0 + })); + builder.addTestResult(result2, mkFormattedResult_({ + testPath: ['s1'], browserId: 'b1', attempt: 1 + })); + + assert.deepEqual(builder.tree.browsers.byId['s1 b1'].versions, ['1', '2']); + }); + it('should collect all ids to test results in browser', () => { builder.addTestResult(mkTestResult_(), mkFormattedResult_({ testPath: ['s1'], browserId: 'b1', attempt: 0 @@ -169,7 +185,8 @@ describe('ResultsTreeBuilder', () => { { id: 's1 b1 0', parentId: 's1 b1', - imageIds: [] + imageIds: [], + metaInfo: {} } ); }); diff --git a/test/unit/lib/tests-tree-builder/gui.js b/test/unit/lib/tests-tree-builder/gui.js index b67185248..f823cadba 100644 --- a/test/unit/lib/tests-tree-builder/gui.js +++ b/test/unit/lib/tests-tree-builder/gui.js @@ -8,7 +8,7 @@ describe('GuiResultsTreeBuilder', () => { let builder; const mkTestResult_ = (result) => { - return _.defaults(result, {status: IDLE, imagesInfo: []}); + return _.defaults(result, {status: IDLE, imagesInfo: [], metaInfo: {}}); }; const mkFormattedResult_ = (result) => { @@ -37,12 +37,12 @@ describe('GuiResultsTreeBuilder', () => { }); describe('"getImagesInfo" method', () => { - it('should return images from tree for passed result', () => { + it('should return images from tree for passed test result id', () => { const formattedRes = mkFormattedResult_({testPath: ['s'], browserId: 'b', attempt: 0}); const imagesInfo = [{stateName: 'image-1'}, {stateName: 'image-2'}]; builder.addTestResult(mkTestResult_({imagesInfo}), formattedRes); - const gotImagesInfo = builder.getImagesInfo(formattedRes); + const gotImagesInfo = builder.getImagesInfo('s b 0'); assert.deepEqual( gotImagesInfo, diff --git a/test/unit/lib/tests-tree-builder/static.js b/test/unit/lib/tests-tree-builder/static.js index e066269e1..2f8cb90fc 100644 --- a/test/unit/lib/tests-tree-builder/static.js +++ b/test/unit/lib/tests-tree-builder/static.js @@ -64,7 +64,6 @@ describe('StaticResultsTreeBuilder', () => { beforeEach(() => { sandbox.stub(StaticResultsTreeBuilder.prototype, 'addTestResult'); sandbox.stub(StaticResultsTreeBuilder.prototype, 'sortTree'); - sandbox.stub(StaticResultsTreeBuilder.prototype, 'convertToOldFormat').returns({}); builder = StaticResultsTreeBuilder.create(); }); @@ -133,23 +132,12 @@ describe('StaticResultsTreeBuilder', () => { }); }); - describe('should return tests tree', () => { - it('in old format by default', () => { - StaticResultsTreeBuilder.prototype.convertToOldFormat.returns('old-format-tree'); + it('should return tests tree', () => { + sandbox.stub(StaticResultsTreeBuilder.prototype, 'tree').get(() => 'tree'); - const {tree} = builder.build([]); + const {tree} = builder.build([]); - assert.equal(tree, 'old-format-tree'); - }); - - it('without formatting', () => { - sandbox.stub(StaticResultsTreeBuilder.prototype, 'tree').get(() => 'tree'); - - const {tree} = builder.build([], {convertToOldFormat: false}); - - assert.notCalled(StaticResultsTreeBuilder.prototype.convertToOldFormat); - assert.equal(tree, 'tree'); - }); + assert.equal(tree, 'tree'); }); describe('should return browsers', () => {