From b19fd61a6dc73d584dc5a893c8c179ea35c1d66f Mon Sep 17 00:00:00 2001 From: unlok Date: Tue, 14 Jul 2020 16:56:49 +0300 Subject: [PATCH] feat: Implemented browser versions for reporter --- hermione.js | 8 -- lib/common-utils.js | 20 +-- lib/constants/browser.js | 3 + lib/constants/database.js | 1 - lib/gui/tool-runner/index.js | 7 +- lib/gui/tool-runner/report-subscriber.js | 7 -- lib/gui/tool-runner/utils.js | 8 +- lib/report-builder/gui.js | 1 - lib/report-builder/static.js | 4 - lib/sqlite-adapter.js | 11 +- .../components/controls/browser-list.js | 114 ++++++++++++++--- .../components/section/section-common.js | 1 + lib/static/modules/actions.js | 6 +- lib/static/modules/custom-queries.js | 6 +- lib/static/modules/database-utils.js | 16 +-- lib/static/modules/group-errors.js | 26 ++-- lib/static/modules/query-params.js | 41 +++++- lib/static/modules/reducers/reporter.js | 103 +++++++++++---- lib/static/modules/utils.js | 118 +++++++++++++----- lib/static/styles.css | 17 ++- lib/test-adapter.js | 4 + test/unit/hermione.js | 16 --- test/unit/lib/common-utils.js | 16 +-- .../lib/gui/tool-runner/report-subsciber.js | 17 --- test/unit/lib/report-builder/static.js | 20 --- .../components/controls/browser-list.js | 35 ++++-- .../unit/lib/static/modules/custom-queries.js | 13 +- test/unit/lib/static/modules/group-errors.js | 50 +++++++- .../lib/static/modules/reducers/reporter.js | 58 ++++++--- test/unit/lib/static/modules/sqlite.js | 3 +- test/unit/lib/static/modules/utils.js | 98 +++++++++------ test/unit/utils.js | 4 +- 32 files changed, 551 insertions(+), 301 deletions(-) create mode 100644 lib/constants/browser.js diff --git a/hermione.js b/hermione.js index 09e556312..8a08d4ce3 100644 --- a/hermione.js +++ b/hermione.js @@ -2,8 +2,6 @@ const os = require('os'); const PQueue = require('p-queue'); - -const utils = require('./lib/common-utils'); const PluginAdapter = require('./lib/plugin-adapter'); const createWorkers = require('./lib/workers/create-workers'); @@ -83,11 +81,5 @@ async function prepare(hermione, reportBuilder, pluginConfig) { hermione.on(hermione.events.RUNNER_END, () => { return Promise.all(promises).then(resolve, reject); }); - - hermione.on(hermione.events.AFTER_TESTS_READ, (collection) => { - const browsers = utils.formatBrowsers(collection); - - reportBuilder.setBrowsers(browsers); - }); }); } diff --git a/lib/common-utils.js b/lib/common-utils.js index 6230aeed1..ee5ded27f 100644 --- a/lib/common-utils.js +++ b/lib/common-utils.js @@ -13,7 +13,6 @@ const { const { DB_MAX_AVAILABLE_PAGE_SIZE, DB_SUITES_TABLE_NAME, - DB_BROWSERS_TABLE_NAME, DB_TYPES } = require('./constants/database'); @@ -34,7 +33,6 @@ exports.isUpdatedStatus = (status) => status === UPDATED; exports.selectAllQuery = (tableName) => `SELECT * FROM ${tableName}`; exports.selectAllSuitesQuery = () => exports.selectAllQuery(DB_SUITES_TABLE_NAME); -exports.selectAllBrowsersQuery = () => exports.selectAllQuery(DB_BROWSERS_TABLE_NAME); exports.suitesTableColumns = [ {name: 'suitePath', type: DB_TYPES.text}, @@ -52,10 +50,6 @@ exports.suitesTableColumns = [ {name: 'timestamp', type: DB_TYPES.int} ]; -exports.browsersTableColumns = [ - {name: 'name', type: DB_TYPES.text} -]; - exports.dbColumnIndexes = exports.suitesTableColumns.reduce((acc, {name}, index) => { acc[name] = index; return acc; @@ -77,8 +71,7 @@ exports.determineStatus = (statuses) => { }; exports.createTablesQuery = () => [ - createTableQuery(DB_SUITES_TABLE_NAME, exports.suitesTableColumns), - createTableQuery(DB_BROWSERS_TABLE_NAME, exports.browsersTableColumns) + createTableQuery(DB_SUITES_TABLE_NAME, exports.suitesTableColumns) ]; function createTableQuery(tableName, columns) { @@ -91,8 +84,7 @@ function createTableQuery(tableName, columns) { exports.mergeTablesQueries = (dbPaths) => { const tablesToMerge = [ - DB_SUITES_TABLE_NAME, - DB_BROWSERS_TABLE_NAME + DB_SUITES_TABLE_NAME ]; const mkMergeQuery = (table) => `INSERT OR IGNORE INTO ${table} SELECT * FROM attached.${table}`; const queries = [].concat( @@ -112,11 +104,3 @@ exports.mergeTablesQueries = (dbPaths) => { exports.compareDatabaseRowsByTimestamp = (row1, row2) => { return row1[exports.dbColumnIndexes.timestamp] - row2[exports.dbColumnIndexes.timestamp]; }; - -exports.formatBrowsers = (collection) => { - return collection - .getBrowsers() - .map((name) => ({ - id: name - })); -}; diff --git a/lib/constants/browser.js b/lib/constants/browser.js new file mode 100644 index 000000000..221371b0b --- /dev/null +++ b/lib/constants/browser.js @@ -0,0 +1,3 @@ +exports.versions = { + UNKNOWN: 'unknown' +}; diff --git a/lib/constants/database.js b/lib/constants/database.js index f01ad3ae7..461cb9fb3 100644 --- a/lib/constants/database.js +++ b/lib/constants/database.js @@ -3,7 +3,6 @@ module.exports = { DB_MAX_AVAILABLE_PAGE_SIZE: 65536, // helps to speed up queries DB_SUITES_TABLE_NAME: 'suites', - DB_BROWSERS_TABLE_NAME: 'browsers', DB_TYPES: { int: 'INT', text: 'TEXT' diff --git a/lib/gui/tool-runner/index.js b/lib/gui/tool-runner/index.js index 1a53f76d2..5f01a5151 100644 --- a/lib/gui/tool-runner/index.js +++ b/lib/gui/tool-runner/index.js @@ -116,10 +116,9 @@ module.exports = class ToolRunner { const {autoRun} = this._guiOpts; this._tree = {...this._reportBuilder.getResult(), gui: true, autoRun}; - const {suites, browsers} = await this._applyReuseData(this._tree.suites); + const {suites} = await this._applyReuseData(this._tree.suites); this._tree.suites = suites; - this._tree.browsers = browsers; } async _applyReuseData(testSuites) { @@ -127,13 +126,13 @@ module.exports = class ToolRunner { return {}; } - const {suites: preparedSuites, browsers} = await this._loadReuseData(); + const {suites: preparedSuites} = await this._loadReuseData(); const suites = _.isEmpty(preparedSuites) ? testSuites : testSuites.map((suite) => applyReuse(preparedSuites)(suite)); - return {suites, browsers}; + return {suites}; } async _loadReuseData() { diff --git a/lib/gui/tool-runner/report-subscriber.js b/lib/gui/tool-runner/report-subscriber.js index fdc6f5473..badebcb99 100644 --- a/lib/gui/tool-runner/report-subscriber.js +++ b/lib/gui/tool-runner/report-subscriber.js @@ -5,7 +5,6 @@ const {RUNNING} = require('../../constants/test-statuses'); const {getSuitePath} = require('../../plugin-utils').getHermioneUtils(); const {findTestResult, withErrorHandling} = require('./utils'); const createWorkers = require('../../workers/create-workers'); -const utils = require('../../../lib/common-utils'); let workers; @@ -107,10 +106,4 @@ module.exports = (hermione, reportBuilder, client, reportPath) => { hermione.on(hermione.events.RUNNER_END, async () => { return client.emit(clientEvents.END); }); - - hermione.on(hermione.events.AFTER_TESTS_READ, (collection) => { - const browsers = utils.formatBrowsers(collection); - - reportBuilder.setBrowsers(browsers); - }); }; diff --git a/lib/gui/tool-runner/utils.js b/lib/gui/tool-runner/utils.js index c42c92598..654c58543 100644 --- a/lib/gui/tool-runner/utils.js +++ b/lib/gui/tool-runner/utils.js @@ -11,8 +11,8 @@ const NestedError = require('nested-error-stacks'); const {logger, logError} = require('../../server-utils'); const constantFileNames = require('../../constants/file-names'); const {findNode, setStatusForBranch} = require('../../static/modules/utils'); -const {formatTestAttempt, formatBrowsersDataFromDb} = require('../../../lib/static/modules/database-utils'); -const {mergeTablesQueries, selectAllSuitesQuery, selectAllBrowsersQuery, compareDatabaseRowsByTimestamp} = require('../../common-utils'); +const {formatTestAttempt} = require('../../../lib/static/modules/database-utils'); +const {mergeTablesQueries, selectAllSuitesQuery, compareDatabaseRowsByTimestamp} = require('../../common-utils'); const {writeDatabaseUrlsFile} = require('../../server-utils'); const formatTestHandler = (browser, test) => { @@ -164,13 +164,11 @@ exports.getDataFromDatabase = (dbPath) => { try { const db = new Database(dbPath, {readonly: true, fileMustExist: true}); const suitesRows = db.prepare(selectAllSuitesQuery()).raw().all(); - const browsersRows = db.prepare(selectAllBrowsersQuery()).raw().all(); - const browsers = formatBrowsersDataFromDb(browsersRows); const suites = parseDatabaseSuitesRows(suitesRows.sort(compareDatabaseRowsByTimestamp)); db.close(); - return {...suites, browsers}; + return suites; } catch (err) { throw new NestedError('Error while getting data from database ', err); } diff --git a/lib/report-builder/gui.js b/lib/report-builder/gui.js index 19b51b77e..c4b55e8d9 100644 --- a/lib/report-builder/gui.js +++ b/lib/report-builder/gui.js @@ -27,7 +27,6 @@ module.exports = class GuiReportBuilder extends StaticReportBuilder { addSkipped(result) { const formattedResult = super.addSkipped(result); - const { suite: { skipComment: comment, diff --git a/lib/report-builder/static.js b/lib/report-builder/static.js index 1f71080e6..89757e79b 100644 --- a/lib/report-builder/static.js +++ b/lib/report-builder/static.js @@ -40,10 +40,6 @@ module.exports = class StaticReportBuilder { : TestAdapter.create(result, this._hermione, this._pluginConfig, status); } - setBrowsers(browsers) { - this._sqliteAdapter.writeBrowsers(browsers); - } - async saveStaticFiles() { const destPath = this._pluginConfig.path; diff --git a/lib/sqlite-adapter.js b/lib/sqlite-adapter.js index 8cd872b23..a65db656a 100644 --- a/lib/sqlite-adapter.js +++ b/lib/sqlite-adapter.js @@ -8,7 +8,7 @@ const debug = require('debug')('html-reporter:sqlite-adapter'); const {createTablesQuery, suitesTableColumns} = require('./common-utils'); const {LOCAL_DATABASE_NAME, DATABASE_URLS_JSON_NAME} = require('./constants/file-names'); -const {DB_SUITES_TABLE_NAME, DB_BROWSERS_TABLE_NAME} = require('./constants/database'); +const {DB_SUITES_TABLE_NAME} = require('./constants/database'); module.exports = class SqliteAdapter { static create(...args) { @@ -59,15 +59,6 @@ module.exports = class SqliteAdapter { this._db.prepare(`INSERT INTO ${DB_SUITES_TABLE_NAME} VALUES (${placeholders})`).run(...values); } - writeBrowsers(browsers) { - const insert = this._db.prepare(`INSERT INTO ${DB_BROWSERS_TABLE_NAME} (name) VALUES (@id)`); - const insertBulk = this._db.transaction((bros) => { - bros.forEach((bro) => insert.run(bro)); - }); - - return insertBulk(browsers); - } - _parseTestResult({suitePath, suiteName, testResult}) { const { name, diff --git a/lib/static/components/controls/browser-list.js b/lib/static/components/controls/browser-list.js index 60df9277a..e5799c156 100644 --- a/lib/static/components/controls/browser-list.js +++ b/lib/static/components/controls/browser-list.js @@ -1,31 +1,98 @@ 'use strict'; import React, {Component} from 'react'; -import {flatten, isEmpty} from 'lodash'; +import {flatten, isEmpty, get, chain, compact} from 'lodash'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import TreeSelect, {SHOW_PARENT} from 'rc-tree-select'; import 'rc-tree-select/assets/index.less'; +const ROOT_NODE_ID = 'ROOT_NODE_ID'; + export default class BrowserList extends Component { static propTypes = { available: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string + id: PropTypes.string, + versions: PropTypes.arrayOf(PropTypes.string) })).isRequired, - selected: PropTypes.arrayOf(PropTypes.string).isRequired, + selected: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string, + versions: PropTypes.arrayOf(PropTypes.string) + })), onChange: PropTypes.func.isRequired } + _buildComplexId(browserId, version) { + return version + ? `${browserId} (${version})` + : browserId; + } + _prepareTreeData() { const {available} = this.props; - const mkNode = (value, parentId) => ({ - pId: parentId, - label: value, - key: value, - value + const mkNode = (parentId, browserId, version) => { + const id = this._buildComplexId(browserId, version); + + return { + pId: parentId, + key: id, + label: id, + value: id, + data: {id, browserId, version} + }; + }; + + const tree = available.map(({id: browserId, versions = []}) => { + if (isEmpty(versions) || versions.length === 1) { + return mkNode(ROOT_NODE_ID, browserId); + } + + const rootNode = mkNode(ROOT_NODE_ID, browserId); + const subNodes = versions.map((version) => mkNode(rootNode.key, browserId, version)); + + return [].concat(rootNode, subNodes); }); - return available.map(({id}) => mkNode(id, 0)); + return flatten(tree); + } + + _buidSelectedItems() { + const {available, selected = []} = this.props; + const selectedItems = selected.map(({id, versions}) => { + const availableNode = available.find((item) => item.id === id); + const shouldDrawOnlyRootNode = get(availableNode, 'versions', []).length === 1 || isEmpty(versions); + + if (shouldDrawOnlyRootNode) { + return this._buildComplexId(id); + } + + return versions.map((version) => this._buildComplexId(id, version)); + }); + + return flatten(selectedItems); + } + + _formatSelectedData(selectedItems) { + const treeDataMap = this._simpleTree.reduce((map, item) => { + map[item.value] = item.data; + + return map; + }, {}); + + return chain(selectedItems) + .reduce((map, id) => { + const {browserId, version} = treeDataMap[id] || {}; + const versions = map[browserId] || []; + + versions.push(version); + + map[browserId] = versions; + + return map; + }, {}) + .map((versions, id) => ({id, versions: compact(versions)})) + .value(); } _shouldNotRender() { @@ -37,17 +104,30 @@ export default class BrowserList extends Component { return (
); } - const {selected, onChange} = this.props; - const tree = this._prepareTreeData(); - const treeDataSimpleMode = {id: 'key', rootPId: 0}; + this._simpleTree = this._prepareTreeData(); + + const treeDataSimpleMode = {id: 'key', rootPId: ROOT_NODE_ID}; + const hasNestedLevels = this._simpleTree.find(node => node.pId !== ROOT_NODE_ID); const removeIcon = (); + const switcherIcon = (item) => { + if (item.isLeaf) { + return; + } + + return item.expanded + ? + : ; + }; + const dropdownClassName = classNames( + {'browserlist_linear': !hasNestedLevels}, + ); return (
} - onChange={onChange} + onChange={(selected) => this.props.onChange(this._formatSelectedData(selected))} maxTagCount={2} />
diff --git a/lib/static/components/section/section-common.js b/lib/static/components/section/section-common.js index da88268db..cdeb3bca5 100644 --- a/lib/static/components/section/section-common.js +++ b/lib/static/components/section/section-common.js @@ -86,6 +86,7 @@ class SectionCommon extends Component { } 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}`); diff --git a/lib/static/modules/actions.js b/lib/static/modules/actions.js index ea95e110f..4680928b8 100644 --- a/lib/static/modules/actions.js +++ b/lib/static/modules/actions.js @@ -257,12 +257,12 @@ export const findSameDiffs = ({suitePath, browser, stateName, fails}) => { }; }; -export const selectBrowsers = (browserIds) => { - setFilteredBrowsers(browserIds); +export const selectBrowsers = (browsers) => { + setFilteredBrowsers(browsers); return triggerViewChanges({ type: actionNames.BROWSERS_SELECTED, - payload: {browserIds} + payload: {browsers} }); }; diff --git a/lib/static/modules/custom-queries.js b/lib/static/modules/custom-queries.js index 3bee52e6b..2225b5edc 100644 --- a/lib/static/modules/custom-queries.js +++ b/lib/static/modules/custom-queries.js @@ -1,6 +1,6 @@ 'use strict'; -import {parseQuery} from './query-params'; +import {parseQuery, decodeBrowsers} from './query-params'; import {pick, has} from 'lodash'; import viewModes from '../../constants/view-modes'; import {config} from '../../constants/defaults'; @@ -10,9 +10,7 @@ const allowedViewModes = new Set(Object.values(viewModes)); export function getViewQuery(queryString) { const query = parseQuery(queryString, {browser: 'filteredBrowsers'}); - if (typeof query.filteredBrowsers === 'string') { - query.filteredBrowsers = [query.filteredBrowsers]; - } + query.filteredBrowsers = decodeBrowsers(query.filteredBrowsers); if (has(query, 'viewMode') && !allowedViewModes.has(query.viewMode)) { query.viewMode = config.defaultView; diff --git a/lib/static/modules/database-utils.js b/lib/static/modules/database-utils.js index 6344ddf4e..b474400af 100644 --- a/lib/static/modules/database-utils.js +++ b/lib/static/modules/database-utils.js @@ -1,8 +1,7 @@ 'use strict'; -const {flatten, uniq} = require('lodash'); const {selectAllQuery, compareDatabaseRowsByTimestamp, dbColumnIndexes} = require('../../common-utils'); -const {DB_SUITES_TABLE_NAME, DB_BROWSERS_TABLE_NAME} = require('../../constants/database'); +const {DB_SUITES_TABLE_NAME} = require('../../constants/database'); function getSuitesTableRows(db) { const databaseRows = getTableRows(db, DB_SUITES_TABLE_NAME); @@ -10,10 +9,6 @@ function getSuitesTableRows(db) { return databaseRows.values.sort(compareDatabaseRowsByTimestamp); } -function getBrowsersTableRows(db) { - return getTableRows(db, DB_BROWSERS_TABLE_NAME).values; -} - function getTableRows(db, tableName) { const [databaseRows] = db.exec(selectAllQuery(tableName)); @@ -26,13 +21,6 @@ function getTableRows(db, tableName) { return databaseRows; } -function formatBrowsersDataFromDb(browsers = []) { - const list = flatten(browsers); - const uniqList = uniq(list); - - return uniqList.map((name) => ({id: name})); -} - function formatTestAttempt(attempt) { const attemptSuitePath = JSON.parse(attempt[dbColumnIndexes.suitePath]); return { @@ -74,8 +62,6 @@ function closeDatabase(db) { module.exports = { getSuitesTableRows, - getBrowsersTableRows, - formatBrowsersDataFromDb, formatTestAttempt, closeDatabase }; diff --git a/lib/static/modules/group-errors.js b/lib/static/modules/group-errors.js index 422ac1bdf..f8a04a6da 100644 --- a/lib/static/modules/group-errors.js +++ b/lib/static/modules/group-errors.js @@ -1,7 +1,7 @@ 'use strict'; import {get, filter} from 'lodash'; -import {isSuiteFailed, isTestNameMatchFilters} from './utils'; +import {isSuiteFailed, isTestNameMatchFilters, shouldShowBrowser} from './utils'; import {isFailStatus, isErroredStatus} from '../../common-utils'; import viewModes from '../../constants/view-modes'; @@ -20,7 +20,6 @@ function groupErrors({suites = {}, viewMode = viewModes.ALL, errorPatterns = [], 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) => { @@ -40,7 +39,7 @@ function extractErrors(rootSuites, showOnlyFailed) { const extract = (suites) => { for (const suite of Object.values(suites)) { const testName = suite.suitePath.join(' '); - const browsersWithError = {}; + const browsersWithError = []; if (suite.browsers) { for (const browser of suite.browsers) { @@ -49,15 +48,21 @@ function extractErrors(rootSuites, showOnlyFailed) { continue; } const retries = [...browser.retries, browser.result]; - const errorsInBrowser = extractErrorsFromRetries(retries); - if (errorsInBrowser.length) { - browsersWithError[browser.name] = errorsInBrowser; + const errors = extractErrorsFromRetries(retries); + + if (errors.length) { + browsersWithError.push({ + browser, + errors + }); } } } + if (Object.keys(browsersWithError).length) { testWithErrors[testName] = browsersWithError; } + if (suite.children) { extract(suite.children); } @@ -93,10 +98,11 @@ function getErrorGroupList(testWithErrors, errorPatterns, filteredBrowsers, test continue; } - for (const [browserName, errors] of Object.entries(browsers)) { - if (filteredBrowsers.length !== 0 && !filteredBrowsers.includes(browserName)) { + 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; @@ -113,8 +119,8 @@ function getErrorGroupList(testWithErrors, errorPatterns, filteredBrowsers, test if (!group.tests.hasOwnProperty(testName)) { group.tests[testName] = []; } - if (!group.tests[testName].includes(browserName)) { - group.tests[testName].push(browserName); + if (!group.tests[testName].includes(browser.name)) { + group.tests[testName].push(browser.name); group.count++; } } diff --git a/lib/static/modules/query-params.js b/lib/static/modules/query-params.js index 4f87dcd1f..3ab80bd97 100644 --- a/lib/static/modules/query-params.js +++ b/lib/static/modules/query-params.js @@ -1,7 +1,7 @@ 'use strict'; import qs from 'qs'; -import {assign} from 'lodash'; +import {assign, isEmpty, compact} from 'lodash'; export function parseQuery(queryString, rename = {}) { if (!queryString || typeof queryString !== 'string') { @@ -53,10 +53,45 @@ export function appendQuery(url, query) { return resultUrl.href; } -export function setFilteredBrowsers(browserIds) { +export function encodeBrowsers(browsers) { + return browsers.map(({id, versions}) => { + const encodedId = encodeURIComponent(id); + + if (isEmpty(versions)) { + return encodedId; + } + + const encodedVersions = versions + .map(encodeURIComponent) + .join(','); + + return `${encodedId}:${encodedVersions}`; + }); +} + +export function decodeBrowsers(browsers) { + const decode = (browser) => { + const [browserName, packedVersions = ''] = browser.split(':'); + const versions = packedVersions + .split(',') + .map(decodeURIComponent); + + return { + id: decodeURIComponent(browserName), + versions: compact(versions) + }; + }; + + return [] + .concat(browsers) + .filter(Boolean) + .map(decode); +} + +export function setFilteredBrowsers(browsers) { const urlExtendedWithBrowsers = appendQuery( window.location.href, - {browser: browserIds} + {browser: encodeBrowsers(browsers)} ); window.history.pushState(null, null, urlExtendedWithBrowsers); diff --git a/lib/static/modules/reducers/reporter.js b/lib/static/modules/reducers/reporter.js index 2f0134f29..6658bfd5a 100644 --- a/lib/static/modules/reducers/reporter.js +++ b/lib/static/modules/reducers/reporter.js @@ -1,6 +1,7 @@ 'use strict'; import url from 'url'; +import _ from 'lodash'; import actionNames from '../action-names'; import defaultState from '../default-state'; import {assign, clone, cloneDeep, filter, find, last, map, merge, reduce, isEmpty} from 'lodash'; @@ -16,22 +17,21 @@ import { import { closeDatabase, formatTestAttempt, - formatBrowsersDataFromDb, - getSuitesTableRows, - getBrowsersTableRows + getSuitesTableRows } from '../database-utils'; import {groupErrors} from '../group-errors'; import * as localStorageWrapper from './helpers/local-storage-wrapper'; import {getViewQuery} from '../custom-queries'; import testStatus from '../../../constants/test-statuses'; 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, browsers, + skips, suites, config, total, updated, passed, failed, skipped, warned, retries, perBrowser, apiValues, gui = false, autoRun, date } = data; @@ -40,16 +40,18 @@ function getInitialState(data) { const {errorPatterns, scaleImages, lazyLoadOffset, defaultView: viewMode} = config; const viewQuery = getViewQuery(window.location.search); - if (isEmpty(viewQuery.filteredBrowsers)) { - viewQuery.filteredBrowsers = map(browsers, 'id'); - } - 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, @@ -281,12 +283,20 @@ function reducer(state = getInitialState(compiledData), action) { return state; } case actionNames.BROWSERS_SELECTED: { - const {browserIds} = action.payload; - const {view} = state; + 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 = browserIds; + view.filteredBrowsers = browsers; - return assign(clone(state), {view}); + return assign(clone(state), {view, groupedErrors}); } default: return state; @@ -313,11 +323,7 @@ function createTestResultsFromDb(state, action) { const suitesRows = getSuitesTableRows(db); const formattedSuites = formatSuitesDataFromDb(suitesRows); - const browsersRows = getBrowsersTableRows(db); - const formattedBrowsers = formatBrowsersDataFromDb(browsersRows); - const browseres = map(formattedBrowsers, 'id'); - const {suites, suitesStats} = formattedSuites; - + const {suites, suitesStats, browsers} = formattedSuites; const {failed, passed, retries, skipped, total, perBrowser} = suitesStats; const suiteIds = { all: getSuiteIds(suites).sort(), @@ -349,10 +355,12 @@ function createTestResultsFromDb(state, action) { perBrowser }, skips: suitesStats.skippedTests, - browsers: formattedBrowsers, + browsers, view: merge(state.view, { - browseres, - filteredBrowsers: viewQuery.filteredBrowsers || browseres + browsers, + filteredBrowsers: _.isEmpty(viewQuery.filteredBrowsers) + ? browsers + : viewQuery.filteredBrowsers }), groupedErrors }; @@ -371,7 +379,15 @@ function populateSuitesTree(attempt, node, suitePath, suitesStats) { comment: browserResult.result.skipReason }); } - updateSuitesStats(suitesStats, attempt.status, {suitePath: attempt.suitePath, browserName: browserResult.name}); + updateSuitesStats( + suitesStats, + attempt.status, + { + suitePath: attempt.suitePath, + browserName: browserResult.name, + browserVersion: browserResult.result.metaInfo.browserVersion + } + ); const browser = find(node.browsers, {name: browserResult.name}); if (!browser) { node.browsers.push(browserResult); @@ -396,6 +412,21 @@ function populateSuitesTree(attempt, node, suitePath, suitesStats) { populateSuitesTree(attempt, child, suitePath, suitesStats); } +function populateBrowsers(browsers, attempt) { + const [child] = attempt.children; + const [browser] = child.browsers; + const {result} = browser; + const browserName = result.name; + + if (!browsers[browserName]) { + browsers[browserName] = new Set(); + } + + const {browserVersion = BrowserVersions.UNKNOWN} = result.metaInfo; + + browsers[browserName].add(browserVersion); +} + function formatSuitesDataFromDb(rows = []) { const suitesStats = { total: 0, @@ -408,6 +439,7 @@ function formatSuitesDataFromDb(rows = []) { passedTestIds: {}, skippedTests: [] }; + const browsers = {}; const suitesTree = {}; for (const attempt of rows) { const formattedAttempt = formatTestAttempt(attempt); @@ -422,11 +454,13 @@ function formatSuitesDataFromDb(rows = []) { } populateSuitesTree(formattedAttempt, suitesTree[suiteId], suitePath, suitesStats); + populateBrowsers(browsers, formattedAttempt); setStatusForBranch(suitesTree, formattedAttempt.suitePath); } return { suites: suitesTree, - suitesStats + suitesStats, + browsers: map(browsers, (versions, id) => ({id, versions: Array.from(versions)})) }; } @@ -557,6 +591,31 @@ function formatSuitesData(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)); } diff --git a/lib/static/modules/utils.js b/lib/static/modules/utils.js index 2bd535cfb..06e928e1a 100644 --- a/lib/static/modules/utils.js +++ b/lib/static/modules/utils.js @@ -3,9 +3,10 @@ const testStatus = require('../../constants/test-statuses'); const {config: {defaultView}} = require('../../constants/defaults'); const viewModes = require('../../constants/view-modes'); +const {versions: BrowserVersions} = require('../../constants/browser'); const url = require('url'); -const {forOwn, pick, isArray, isObject, find, get, values, isEmpty} = require('lodash'); +const {isArray, isObject, find, get, values, isEmpty, forEach, flatten, keys} = require('lodash'); const { isIdleStatus, @@ -145,20 +146,24 @@ function setStatusForBranch(nodes, suitePath) { } function getStats(stats, filteredBrowsers) { - if (filteredBrowsers.length === 0 || !stats.perBrowser) { + if (isEmpty(filteredBrowsers) || isEmpty(stats.perBrowser)) { return stats.all; } const resStats = {}; - const neededBrowserStats = pick(stats.perBrowser, filteredBrowsers); - - values(neededBrowserStats).forEach((browserStat) => { - forOwn(browserStat, (value, key) => { - if (resStats[key] === undefined) { - resStats[key] = value; - } else { - resStats[key] += value; - } + 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; }); }); @@ -222,16 +227,26 @@ function shouldSuiteBeShown({ } 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; + } - const strictMatchNames = strictTestNameFilters.length === 0 - || strictTestNameFilters.includes(suiteFullPath); + return suite.browsers.some((browser) => { + const browserResultStatus = get(browser, 'result.status'); + const shouldShowForViewMode = isStatusMatchViewMode(browserResultStatus, viewMode); - const matchBrowsers = filteredBrowsers.length === 0 || suite.browsers.some((browser) => - filteredBrowsers.includes(browser.name) && isStatusMatchViewMode(get(browser, 'result.status'), viewMode)); + if (!shouldShowForViewMode) { + return false; + } + + return shouldShowBrowser(browser, filteredBrowsers); + }); + }; - return matchName && strictMatchNames && matchBrowsers; + return matchName && strictMatchNames && shouldShowSuite(); } function shouldBrowserBeShown({ @@ -252,10 +267,33 @@ function shouldBrowserBeShown({ errorGroupBrowsers = errorGroupTests[fullTestName]; } - const matchFilteredBrowsers = filteredBrowsers.length === 0 || filteredBrowsers.includes(name); const matchErrorGroupBrowsers = errorGroupBrowsers.length === 0 || errorGroupBrowsers.includes(name); - return matchFilteredBrowsers && matchErrorGroupBrowsers; + 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 = []) { @@ -302,31 +340,43 @@ function isUrl(str) { } function updateSuitesStats(stats, status, attemptInfo) { - const {suitePath, browserName} = attemptInfo; + const {suitePath, browserName, browserVersion} = attemptInfo; const attemptId = suitePath.join('') + browserName; + const version = browserVersion || BrowserVersions.UNKNOWN; if (!stats.perBrowser[browserName]) { - stats.perBrowser[browserName] = {total: 0, passed: 0, failed: 0, skipped: 0, retries: 0}; + stats.perBrowser[browserName] = {}; } + + if (!stats.perBrowser[browserName][version]) { + stats.perBrowser[browserName][version] = { + total: 0, + passed: 0, + failed: 0, + skipped: 0, + retries: 0 + }; + } + switch (status) { case testStatus.FAIL: case testStatus.ERROR: { if (stats.failedTestIds[attemptId]) { stats.retries++; - stats.perBrowser[browserName].retries++; + stats.perBrowser[browserName][version].retries++; return; } stats.failedTestIds[attemptId] = true; stats.failed++; stats.total++; - stats.perBrowser[browserName].failed++; - stats.perBrowser[browserName].total++; + stats.perBrowser[browserName][version].failed++; + stats.perBrowser[browserName][version].total++; return; } case testStatus.SUCCESS: { if (stats.passedTestIds[attemptId]) { stats.retries++; - stats.perBrowser[browserName].retries++; + stats.perBrowser[browserName][version].retries++; return; } if (stats.failedTestIds[attemptId]) { @@ -334,28 +384,29 @@ function updateSuitesStats(stats, status, attemptInfo) { stats.failed--; stats.passed++; stats.retries++; - stats.perBrowser[browserName].failed--; - stats.perBrowser[browserName].passed++; - stats.perBrowser[browserName].retries++; + stats.perBrowser[browserName][version].failed--; + stats.perBrowser[browserName][version].passed++; + stats.perBrowser[browserName][version].retries++; return; } stats.passedTestIds[attemptId] = true; stats.passed++; stats.total++; - stats.perBrowser[browserName].passed++; - stats.perBrowser[browserName].total++; + stats.perBrowser[browserName][version].passed++; + stats.perBrowser[browserName][version].total++; return; } case testStatus.SKIPPED: { stats.skipped++; + stats.perBrowser[browserName][version].skipped++; if (stats.failedTestIds[attemptId]) { delete stats.failedTestIds[attemptId]; stats.failed--; - stats.perBrowser[browserName].failed--; + stats.perBrowser[browserName][version].failed--; return; } stats.total++; - stats.perBrowser[browserName].total++; + stats.perBrowser[browserName][version].total++; } } } @@ -397,5 +448,6 @@ module.exports = { updateSuitesStats, filterSuites, isTestNameMatchFilters, - getHttpErrorMessage + getHttpErrorMessage, + shouldShowBrowser }; diff --git a/lib/static/styles.css b/lib/static/styles.css index 323586ed7..2290eaef6 100644 --- a/lib/static/styles.css +++ b/lib/static/styles.css @@ -42,10 +42,17 @@ main.container { margin-bottom: 15px; } -.rc-tree-select-tree-switcher { +.browserlist_linear .rc-tree-select-tree-switcher { display: none!important; } +.rc-tree-select-tree .rc-tree-select-tree-treenode span.rc-tree-select-tree-switcher { + width: 13px; + height: 15px; + margin-right: 0; + background-image: none; +} + .rc-tree-select-tree-checkbox { background-image: none!important; } @@ -71,6 +78,14 @@ main.container { cursor: pointer; padding-left: 2px; } +.browserlist__icon-tree { + color: #424242; + font-size: 13px; +} + +.browserlist__icon-tree:hover { + color: #000; +} .rc-tree-select-tree-checkbox-checked::before, .rc-tree-select-tree-checkbox-indeterminate::before { diff --git a/lib/test-adapter.js b/lib/test-adapter.js index 67004017b..c50938e3d 100644 --- a/lib/test-adapter.js +++ b/lib/test-adapter.js @@ -42,6 +42,10 @@ module.exports = class TestAdapter { this._errorDetails = undefined; this._timestamp = this._testResult.timestamp; + const browserVersion = _.get(this._testResult, 'meta.browserVersion', this._testResult.browserVersion); + + _.set(this._testResult, 'meta.browserVersion', browserVersion); + if (utils.shouldUpdateAttempt(status)) { testsAttempts.set(this._testId, _.isUndefined(testsAttempts.get(this._testId)) ? 0 : testsAttempts.get(this._testId) + 1); } diff --git a/test/unit/hermione.js b/test/unit/hermione.js index 545d84b04..e8a4b5273 100644 --- a/test/unit/hermione.js +++ b/test/unit/hermione.js @@ -6,7 +6,6 @@ const HermioneReporter = require('../../hermione'); const PluginAdapter = require('lib/plugin-adapter'); const StaticReportBuilder = require('lib/report-builder/static'); const utils = require('lib/server-utils'); -const commonUtils = require('lib/common-utils'); const {stubTool} = require('./utils'); describe('lib/hermione', () => { @@ -107,7 +106,6 @@ describe('lib/hermione', () => { sandbox.stub(StaticReportBuilder.prototype, 'addRetry'); sandbox.stub(StaticReportBuilder.prototype, 'saveStaticFiles'); sandbox.stub(StaticReportBuilder.prototype, 'finalize'); - sandbox.stub(StaticReportBuilder.prototype, 'setBrowsers'); sandbox.stub(StaticReportBuilder.prototype, 'init'); @@ -283,18 +281,4 @@ describe('lib/hermione', () => { saveDiffTo, sinon.match.instanceOf(ImageDiffError), sinon.match('/report/plain') ); }); - - it('should set browsers to the report builder', async () => { - await initReporter_(); - - const formatedBrowsers = [{id: 'bro1'}]; - const collection = sandbox.stub(); - - sandbox.stub(commonUtils, 'formatBrowsers').returns(formatedBrowsers); - - hermione.emit(events.AFTER_TESTS_READ, collection); - - assert.calledOnceWith(commonUtils.formatBrowsers, collection); - assert.calledOnceWith(StaticReportBuilder.prototype.setBrowsers, formatedBrowsers); - }); }); diff --git a/test/unit/lib/common-utils.js b/test/unit/lib/common-utils.js index 6a2a7de66..984b268fc 100644 --- a/test/unit/lib/common-utils.js +++ b/test/unit/lib/common-utils.js @@ -1,7 +1,7 @@ 'use strict'; const sinon = require('sinon'); -const {determineStatus, formatBrowsers} = require('lib/common-utils'); +const {determineStatus} = require('lib/common-utils'); const {SUCCESS, IDLE} = require('lib/constants/test-statuses'); describe('common-utils', () => { @@ -22,18 +22,4 @@ describe('common-utils', () => { assert.equal(status, SUCCESS); }); }); - - describe('formatBrowsers', () => { - it('should format a linear browser list into appropriate structure by collection', () => { - const collectionStub = { - getBrowsers: sandbox.stub().returns(['bro1', 'bro2']) - }; - const browsers = formatBrowsers(collectionStub); - - assert.deepEqual(browsers, [ - {id: 'bro1'}, - {id: 'bro2'} - ]); - }); - }); }); diff --git a/test/unit/lib/gui/tool-runner/report-subsciber.js b/test/unit/lib/gui/tool-runner/report-subsciber.js index e69b03158..fd4022049 100644 --- a/test/unit/lib/gui/tool-runner/report-subsciber.js +++ b/test/unit/lib/gui/tool-runner/report-subsciber.js @@ -6,7 +6,6 @@ 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 commonUtils = require('lib/common-utils'); const {stubTool, stubConfig} = require('test/unit/utils'); describe('lib/gui/tool-runner/hermione/report-subscriber', () => { @@ -35,7 +34,6 @@ describe('lib/gui/tool-runner/hermione/report-subscriber', () => { reportBuilder = sinon.createStubInstance(GuiReportBuilder); sandbox.stub(GuiReportBuilder, 'create').returns(reportBuilder); reportBuilder.format.returns(mkTestAdapterStub_()); - reportBuilder.setBrowsers.returns(reportBuilder); sandbox.stub(utils, 'findTestResult'); const findTestResult = sandbox.stub(); @@ -130,19 +128,4 @@ describe('lib/gui/tool-runner/hermione/report-subscriber', () => { assert.callOrder(formattedResult.saveTestImages, reportBuilder.addFail); }); }); - - describe('AFTER_TESTS_READ', () => { - it('should set browsers to the report builder', async () => { - const hermione = mkHermione_(); - const collection = sandbox.stub(); - const formatedBrowsers = [{id: 'bro1'}]; - - sandbox.stub(commonUtils, 'formatBrowsers').returns(formatedBrowsers); - reportSubscriber(hermione, reportBuilder, client, ''); - hermione.emit(hermione.events.AFTER_TESTS_READ, collection); - - assert.calledOnceWith(commonUtils.formatBrowsers, collection); - assert.calledOnceWith(reportBuilder.setBrowsers, formatedBrowsers); - }); - }); }); diff --git a/test/unit/lib/report-builder/static.js b/test/unit/lib/report-builder/static.js index 504d102b0..43468cdae 100644 --- a/test/unit/lib/report-builder/static.js +++ b/test/unit/lib/report-builder/static.js @@ -160,26 +160,6 @@ describe('StaticReportBuilder', () => { }); }); - describe('setBrowsers', () => { - it('should write browsers to db when the method has called', async () => { - const reportBuilder = await mkStaticReportBuilder_(); - - await reportBuilder.setBrowsers([ - {id: 'chrome'}, - {id: 'firefox'} - ]); - - const db = new Database(TEST_DB_PATH); - const res = db.prepare('SELECT * from browsers').all(); - db.close(); - - assert.deepEqual(res, [ - {name: 'chrome'}, - {name: 'firefox'} - ]); - }); - }); - describe('finalization', () => { let reportBuilder; diff --git a/test/unit/lib/static/components/controls/browser-list.js b/test/unit/lib/static/components/controls/browser-list.js index 39cecb16e..f8933fba6 100644 --- a/test/unit/lib/static/components/controls/browser-list.js +++ b/test/unit/lib/static/components/controls/browser-list.js @@ -26,8 +26,8 @@ describe('', () => { {id: 'bro3'} ], selected: [ - 'bro2', - 'bro3' + {id: 'bro2'}, + {id: 'bro3'} ] }; const component = mount(); @@ -37,14 +37,32 @@ describe('', () => { assert.equal(component.find('.rc-tree-select-selection-item').at(1).text(), 'bro3'); }); - it('should trigger "change" event when selected items have changed', () => { + it('should create nested checkboxes for versions', () => { const props = { available: [ - {id: 'bro1'}, - {id: 'bro2'} + {id: 'bro1', versions: ['v1', 'v2', 'v3']} + ], + selected: [ + {id: 'bro1', versions: ['v1', 'v2']} + ] + }; + const component = mount(); + + assert.equal(component.find('.rc-tree-select-selection-item').at(0).text(), 'bro1 (v1)'); + assert.equal(component.find('.rc-tree-select-selection-item').at(1).text(), 'bro1 (v2)'); + }); + + it('should trigger "change" event with selected browsers and versions', () => { + const props = { + available: [ + {id: 'bro'}, + {id: 'bro1', versions: []}, + {id: 'bro2', versions: ['v1', 'v2']} ], selected: [ - 'bro2' + {id: 'bro'}, + {id: 'bro1', versions: []}, + {id: 'bro2', versions: ['v1']} ], onChange: sandbox.spy() }; @@ -53,6 +71,9 @@ describe('', () => { component.find('.rc-tree-select-selection-item-remove').first().simulate('click'); assert.equal(props.onChange.callCount, 1); - assert.equal(props.onChange.firstCall.lastArg.triggerValue, 'bro2'); + assert.deepEqual(props.onChange.firstCall.lastArg, [ + {id: 'bro1', versions: []}, + {id: 'bro2', versions: ['v1']} + ]); }); }); diff --git a/test/unit/lib/static/modules/custom-queries.js b/test/unit/lib/static/modules/custom-queries.js index 78e4e22e7..f754d7720 100644 --- a/test/unit/lib/static/modules/custom-queries.js +++ b/test/unit/lib/static/modules/custom-queries.js @@ -4,16 +4,18 @@ import viewModes from 'lib/constants/view-modes'; describe('lib/static/modules/query-params', () => { describe('getViewQuery', () => { - it('returns empty query object when no query params exist', () => { + it('returns initial query object when no query params exist', () => { const query = getViewQuery(''); - assert.deepStrictEqual(query, {}); + assert.deepStrictEqual(query, { + filteredBrowsers: [] + }); }); it('parses specified view-related values', () => { const query = getViewQuery([ 'browser=safari%20browser', - 'browser=firefox', + 'browser=firefox:23', 'testNameFilter=test name', 'retryIndex=10', 'viewMode=all', @@ -23,7 +25,10 @@ describe('lib/static/modules/query-params', () => { ].join('&')); assert.deepStrictEqual(query, { - filteredBrowsers: ['safari browser', 'firefox'], + filteredBrowsers: [ + {id: 'safari browser', versions: []}, + {id: 'firefox', versions: ['23']} + ], testNameFilter: 'test name', retryIndex: 10, viewMode: viewModes.ALL, diff --git a/test/unit/lib/static/modules/group-errors.js b/test/unit/lib/static/modules/group-errors.js index 32733c99f..5c7112518 100644 --- a/test/unit/lib/static/modules/group-errors.js +++ b/test/unit/lib/static/modules/group-errors.js @@ -497,7 +497,7 @@ describe('static/modules/group-errors', () => { const result = groupErrors({ suites, - filteredBrowsers: ['browser-one'] + filteredBrowsers: [{id: 'browser-one'}] }); assert.deepEqual(result, [ @@ -512,6 +512,54 @@ describe('static/modules/group-errors', () => { ]); }); + 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'] + } + } + ]); + }); + it('should group by regexp', () => { const suites = [ mkSuiteTree({ diff --git a/test/unit/lib/static/modules/reducers/reporter.js b/test/unit/lib/static/modules/reducers/reporter.js index 1045b5445..31811a1e8 100644 --- a/test/unit/lib/static/modules/reducers/reporter.js +++ b/test/unit/lib/static/modules/reducers/reporter.js @@ -1,6 +1,7 @@ '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 {mkStorage} from '../../../../utils'; @@ -114,7 +115,38 @@ describe('lib/static/modules/reducers', () => { const newState = reducer(undefined, action); - assert.deepStrictEqual(newState.view.filteredBrowsers, ['firefox', 'safari']); + 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'] + }]); }); }); @@ -325,7 +357,7 @@ describe('lib/static/modules/reducers', () => { 'smalltest1', 'browser', 'url', - JSON.stringify({muted: false}), + JSON.stringify({muted: false, browserVersion: '1.2'}), 'description', JSON.stringify({message: 'error message', stack: 'error stack', history: 'some history'}), 'skipReason', @@ -340,7 +372,7 @@ describe('lib/static/modules/reducers', () => { 'smalltest2', 'browser', 'url', - JSON.stringify({muted: false}), + JSON.stringify({muted: false, browserVersion: '1.1'}), 'description', JSON.stringify({message: 'error message', stack: 'error stack', history: 'some history'}), 'skipReason', @@ -351,14 +383,8 @@ describe('lib/static/modules/reducers', () => { 0 // timestamp ] ]; - const browsersValues = [ - ['chrome'], - ['firefox'] - ]; const db = { - exec: sinon.stub() - .onFirstCall().returns([{values: suitesValues}]) - .onSecondCall().returns([{values: browsersValues}]) + exec: sinon.stub().onFirstCall().returns([{values: suitesValues}]) }; const action = { type: actionNames.FETCH_DB, @@ -379,10 +405,14 @@ describe('lib/static/modules/reducers', () => { assert.match(newState.suites['test'].children[0].browsers[0].retries.length, 1); assert.match(newState.suites['test'].children[1].browsers[0].retries.length, 0); - assert.deepEqual(newState.browsers, [ - {id: 'chrome'}, - {id: 'firefox'} - ]); + assert.deepEqual(newState.browsers, [{ + id: 'browser', + versions: [ + 'unknown', + '1.1', + '1.2' + ] + }]); }); }); diff --git a/test/unit/lib/static/modules/sqlite.js b/test/unit/lib/static/modules/sqlite.js index 1d2019a89..f5307eff2 100644 --- a/test/unit/lib/static/modules/sqlite.js +++ b/test/unit/lib/static/modules/sqlite.js @@ -220,7 +220,7 @@ describe('lib/static/modules/sqlite', () => { assert.calledTwice(SQL.Database.prototype.close); }); - it('should merge both "suites" and "browsers" tables', async () => { + it('should merge both "suites" tables', async () => { const data1 = new ArrayBuffer(1); const data2 = new ArrayBuffer(1); @@ -230,7 +230,6 @@ describe('lib/static/modules/sqlite', () => { .flatten(SQL.Database.prototype.run.args) .join(' '); - assert.include(rawQueries, 'browsers'); assert.include(rawQueries, 'suites'); }); }); diff --git a/test/unit/lib/static/modules/utils.js b/test/unit/lib/static/modules/utils.js index 81b0c1a3e..c9600fa53 100644 --- a/test/unit/lib/static/modules/utils.js +++ b/test/unit/lib/static/modules/utils.js @@ -371,39 +371,63 @@ describe('static/modules/utils', () => { const defaultSuite = mkSuite({ children: [ mkState({ - browsers: [mkBrowserResult({name: 'first-bro'})] + browsers: [mkBrowserResult({name: 'bro1'})] }) ] }); it('should be true if browser id is equal', () => { - assert.isTrue(utils.shouldSuiteBeShown({suite: defaultSuite, filteredBrowsers: ['first-bro']})); + 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: ['first']})); + 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: ['second-bro']})); + 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: 'first-bro'})], + browsers: [mkBrowserResult({name: 'bro1'})], children: [ mkState({ - browsers: [mkBrowserResult({name: 'second-bro'})] + browsers: [mkBrowserResult({name: 'bro2'})] }) ] }) ] }); - assert.isTrue(utils.shouldSuiteBeShown({suite, filteredBrowsers: ['first-bro']})); - assert.isTrue(utils.shouldSuiteBeShown({suite, filteredBrowsers: ['second-bro']})); + 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']}]})); }); }); @@ -477,26 +501,18 @@ describe('static/modules/utils', () => { retries: 30 }, perBrowser: { - 'first-bro': { - total: 15, - passed: 10, - failed: 5, - skipped: 0, - retries: 10 + bro1: { + ver1: {failed: 1, passed: 1}, + ver2: {failed: 1, passed: 1} }, - 'second-bro': { - total: 15, - passed: 5, - failed: 0, - skipped: 10, - retries: 15 + bro2: { + ver1: {failed: 1, passed: 1}, + ver2: {failed: 1, passed: 1} }, - 'third-bro': { - total: 10, - passed: 5, - failed: 5, - skipped: 0, - retries: 5 + bro3: { + ver1: {failed: 1, passed: 1}, + ver2: {failed: 1, passed: 1}, + ver3: {failed: 1, passed: 1} } } }; @@ -514,26 +530,32 @@ describe('static/modules/utils', () => { }); it('should return correct statistics for one filtered browser', () => { - const stats = utils.getStats(inputStats, ['first-bro']); + const stats = utils.getStats(inputStats, [{id: 'bro1'}]); assert.deepEqual(stats, { - total: 15, - passed: 10, - failed: 5, - skipped: 0, - retries: 10 + passed: 2, + failed: 2 }); }); it('should return correct statistics for several filtered browsers', () => { - const stats = utils.getStats(inputStats, ['first-bro', 'second-bro']); + const stats = utils.getStats(inputStats, [{id: 'bro1'}, {id: 'bro2'}]); assert.deepEqual(stats, { - total: 30, - passed: 15, - failed: 5, - skipped: 10, - retries: 25 + 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 }); }); }); diff --git a/test/unit/utils.js b/test/unit/utils.js index ffa012683..4cd5cb3ef 100644 --- a/test/unit/utils.js +++ b/test/unit/utils.js @@ -50,7 +50,7 @@ function mkState(opts = {}) { function mkBrowserResult(opts = {}) { return _.defaults(opts, { name: 'default-bro', - result: mkTestResult({name: opts.name}), + result: mkTestResult({name: opts.name, browserVersion: opts.browserVersion}), retries: [] }); } @@ -59,7 +59,7 @@ function mkTestResult(result) { return _.defaults(result, { name: 'default-bro', suiteUrl: '', - metaInfo: {}, + metaInfo: {browserVersion: result.browserVersion}, imagesInfo: [], status: 'idle', attempt: 0