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
- ?