diff --git a/lib/common-utils.js b/lib/common-utils.js index b1d1fcbc1..08796e86e 100644 --- a/lib/common-utils.js +++ b/lib/common-utils.js @@ -15,6 +15,7 @@ const { RUNNING, QUEUED } = require('./constants/test-statuses'); +const {UNCHECKED, INDETERMINATE, CHECKED} = require('./constants/checked-statuses'); exports.getShortMD5 = (str) => { return crypto.createHash('md5').update(str, 'ascii').digest('hex').substr(0, 7); @@ -115,3 +116,8 @@ function isRelativeUrl(url) { return true; } } + +exports.isCheckboxChecked = (status) => status == CHECKED; // eslint-disable-line eqeqeq +exports.isCheckboxIndeterminate = (status) => status == INDETERMINATE; // eslint-disable-line eqeqeq +exports.isCheckboxUnchecked = (status) => status == UNCHECKED; // eslint-disable-line eqeqeq +exports.getToggledCheckboxState = (status) => exports.isCheckboxChecked(status) ? UNCHECKED : CHECKED; diff --git a/lib/constants/checked-statuses.js b/lib/constants/checked-statuses.js new file mode 100644 index 000000000..41ed35620 --- /dev/null +++ b/lib/constants/checked-statuses.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = { + UNCHECKED: 0, + INDETERMINATE: 0.5, + CHECKED: 1 +}; diff --git a/lib/static/components/bullet.js b/lib/static/components/bullet.js new file mode 100644 index 000000000..196fe4777 --- /dev/null +++ b/lib/static/components/bullet.js @@ -0,0 +1,30 @@ +import React from 'react'; +import classNames from 'classnames'; +import {Checkbox} from 'semantic-ui-react'; +import PropTypes from 'prop-types'; +import {isCheckboxChecked, isCheckboxIndeterminate} from '../../common-utils'; +import {CHECKED, INDETERMINATE, UNCHECKED} from '../../constants/checked-statuses'; +import useLocalStorage from '../hooks/useLocalStorage'; + +const Bullet = ({status, onClick, className}) => { + const [isCheckbox] = useLocalStorage('showCheckboxes', false); + + if (!isCheckbox) { + return ; + } + + return ; +}; + +Bullet.propTypes = { + status: PropTypes.oneOf([CHECKED, UNCHECKED, INDETERMINATE]), + onClick: PropTypes.func, + bulletClassName: PropTypes.string +}; + +export default Bullet; diff --git a/lib/static/components/controls/common-filters.js b/lib/static/components/controls/common-filters.js index 0e914b6ac..d6b7bda5b 100644 --- a/lib/static/components/controls/common-filters.js +++ b/lib/static/components/controls/common-filters.js @@ -6,11 +6,12 @@ import {connect} from 'react-redux'; import * as actions from '../../modules/actions'; import TestNameFilterInput from './test-name-filter-input'; import StrictMatchFilterInput from './strict-match-filter-input'; +import ShowCheckboxesInput from './show-checkboxes-input'; import BrowserList from './browser-list'; class CommonFilters extends Component { render() { - const {filteredBrowsers, browsers, actions} = this.props; + const {filteredBrowsers, browsers, gui, actions} = this.props; return (
@@ -21,12 +22,13 @@ class CommonFilters extends Component { /> + {gui && }
); } } export default connect( - ({view, browsers}) => ({filteredBrowsers: view.filteredBrowsers, browsers}), + ({view, browsers, gui}) => ({filteredBrowsers: view.filteredBrowsers, browsers, gui}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(CommonFilters); diff --git a/lib/static/components/controls/gui-controls.js b/lib/static/components/controls/gui-controls.js index 59522129c..ef7a058d5 100644 --- a/lib/static/components/controls/gui-controls.js +++ b/lib/static/components/controls/gui-controls.js @@ -9,7 +9,6 @@ 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'; @@ -17,40 +16,16 @@ class GuiControls extends Component { static propTypes = { // from store running: PropTypes.bool.isRequired, - processing: PropTypes.bool.isRequired, - stopping: 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, failedTests} = this.props; - - return actions.runFailedTests(failedTests); + stopping: PropTypes.bool.isRequired } render() { - const {actions, allRootSuiteIds, failedRootSuiteIds, running, autoRun, processing, stopping} = this.props; + const {actions, running, stopping} = this.props; return (
- - + { - return { - running: state.running, - processing: state.processing, - stopping: state.stopping, - autoRun: state.autoRun, - allRootSuiteIds: state.tree.suites.allRootIds, - failedRootSuiteIds: state.tree.suites.failedRootIds, - failedTests: getFailedTests(state) - }; - }, + ({running, stopping}) => ({running, stopping}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(GuiControls); diff --git a/lib/static/components/controls/run-button.js b/lib/static/components/controls/run-button.js deleted file mode 100644 index 3d4ca3686..000000000 --- a/lib/static/components/controls/run-button.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import ControlButton from './control-button'; - -export default class RunButton extends Component { - static propTypes = { - handler: PropTypes.func.isRequired, - autoRun: PropTypes.bool.isRequired, - isDisabled: PropTypes.bool, - isRunning: PropTypes.bool - } - - componentDidMount() { - if (this.props.autoRun) { - this.props.handler(); - } - } - - render() { - const {handler, isDisabled, isRunning} = this.props; - - return (); - } -} diff --git a/lib/static/components/controls/run-button/index.js b/lib/static/components/controls/run-button/index.js new file mode 100644 index 000000000..81bc7b6dc --- /dev/null +++ b/lib/static/components/controls/run-button/index.js @@ -0,0 +1,129 @@ +'use strict'; + +import React, {useEffect, useState} from 'react'; +import {bindActionCreators} from 'redux'; +import {isEmpty} from 'lodash'; +import {connect} from 'react-redux'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import * as actions from '../../../modules/actions'; +import Popup from '../../popup'; +import {getFailedTests, getCheckedTests} from '../../../modules/selectors/tree'; +import useLocalStorage from '../../../hooks/useLocalStorage'; + +import './index.styl'; + +const RunMode = Object.freeze({ + ALL: 'All', + FAILED: 'Failed', + CHECKED: 'Checked' +}); + +const RunButton = ({actions, autoRun, isDisabled, isRunning, failedTests, checkedTests}) => { + const [mode, setMode] = useState(RunMode.ALL); + const [showCheckboxes] = useLocalStorage('showCheckboxes', false); + + const btnClassName = classNames('btn', {'button_blink': isRunning}); + + const shouldDisableFailed = isEmpty(failedTests); + const shouldDisableChecked = !showCheckboxes || isEmpty(checkedTests); + + const selectAllTests = () => setMode(RunMode.ALL); + const selectFailedTests = () => !shouldDisableFailed && setMode(RunMode.FAILED); + const selectCheckedTests = () => !shouldDisableChecked && setMode(RunMode.CHECKED); + + const runAllTests = () => actions.runAllTests(); + const runFailedTests = () => actions.runFailedTests(failedTests); + const runCheckedTests = () => actions.retrySuite(checkedTests); + + const handleRunClick = () => { + const action = { + [RunMode.ALL]: runAllTests, + [RunMode.FAILED]: runFailedTests, + [RunMode.CHECKED]: runCheckedTests + }[mode]; + + action(); + }; + + useEffect(() => { + if (autoRun) { + runAllTests(); + } + }, []); + + useEffect(() => { + selectCheckedTests(); + }, [shouldDisableChecked]); + + useEffect(() => { + const shouldResetFailedMode = mode === RunMode.FAILED && shouldDisableFailed; + const shouldResetCheckedMode = mode === RunMode.CHECKED && shouldDisableChecked; + + if (shouldResetFailedMode || shouldResetCheckedMode) { + setMode(RunMode.ALL); + } + }, [shouldDisableFailed, shouldDisableChecked]); + + return ( +
+ + {!isDisabled && } + > +
    +
  • + {RunMode.ALL} +
  • +
  • {RunMode.FAILED} +
  • +
  • + {RunMode.CHECKED} +
  • +
+
} +
+ ); +}; + +RunButton.propTypes = { + // from store + autoRun: PropTypes.bool.isRequired, + isDisabled: PropTypes.bool, + isRunning: PropTypes.bool, + failedTests: PropTypes.arrayOf(PropTypes.shape({ + testName: PropTypes.string, + browserName: PropTypes.string + })).isRequired, + checkedTests: PropTypes.arrayOf(PropTypes.shape({ + testName: PropTypes.string, + browserName: PropTypes.string + })).isRequired +}; + +export default connect( + (state) => { + const autoRun = state.autoRun; + const allRootSuiteIds = state.tree.suites.allRootIds; + const processing = state.processing; + const isDisabled = !allRootSuiteIds.length || processing; + const isRunning = state.running; + const failedTests = getFailedTests(state); + const checkedTests = getCheckedTests(state); + + return {autoRun, isDisabled, isRunning, failedTests, checkedTests}; + }, + (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) +)(RunButton); diff --git a/lib/static/components/controls/run-button/index.styl b/lib/static/components/controls/run-button/index.styl new file mode 100644 index 000000000..1bb4670aa --- /dev/null +++ b/lib/static/components/controls/run-button/index.styl @@ -0,0 +1,49 @@ +.run-button { + display: inline-flex; + align-items: center; + cursor: pointer; + width: 143px; + font-size: 11px; + line-height: 11px; + border: 1px solid #ccc; + border-radius: 2px; + background-color: #ffeba0; + + .btn { + flex-grow: 1; + height: 100%; + background-color: #ffeba0; + cursor: pointer; + border: none; + } + + .run-button__dropdown { + font-family: Dropdown; + + &::before { + content: '\f0d7'; + padding: 5px; + border-left: 1px solid #ccc; + } + } + + .popup__content { + padding: 0; + } + + .run-mode { + padding: 0; + + .run-mode__item { + font-size: 13px; + padding: 5px 10px; + list-style: none; + user-select: none; + + &.run-mode__item_disabled { + color: #939393; + cursor: auto; + } + } + } +} diff --git a/lib/static/components/controls/show-checkboxes-input.js b/lib/static/components/controls/show-checkboxes-input.js new file mode 100644 index 000000000..1e485d45b --- /dev/null +++ b/lib/static/components/controls/show-checkboxes-input.js @@ -0,0 +1,24 @@ +'use strict'; + +import React from 'react'; +import {Checkbox} from 'semantic-ui-react'; +import useLocalStorage from '../../hooks/useLocalStorage'; + +const ShowCheckboxesInput = () => { + const [showCheckboxes, setShowCheckboxes] = useLocalStorage('showCheckboxes', false); + + const onChange = () => setShowCheckboxes(!showCheckboxes); + + return ( +
+ +
+ ); +}; + +export default ShowCheckboxesInput; diff --git a/lib/static/components/controls/strict-match-filter-input.js b/lib/static/components/controls/strict-match-filter-input.js index e9702dc82..bf785041b 100644 --- a/lib/static/components/controls/strict-match-filter-input.js +++ b/lib/static/components/controls/strict-match-filter-input.js @@ -17,7 +17,7 @@ const StrictMatchFilterInput = ({strictMatchFilter, actions}) => { }; return ( -
+
{ + const {name, pattern, testCount, resultCount} = group; - render() { - const {group, isActive, onClick} = this.props; - const {name, pattern, testCount, resultCount} = group; + const body = isActive ? ( +
+ +
+ ) : null; - const body = isActive ? ( -
- -
- ) : null; - - const className = classNames( - 'tests-group', - {'tests-group_collapsed': !isActive} - ); - - return ( -
-
- {name} -  ({`tests: ${testCount}, runs: ${resultCount}`}) -
- {body} + const className = classNames( + 'tests-group', + {'tests-group_collapsed': !isActive} + ); + + const onToggleCheckbox = (e) => { + e.stopPropagation(); + + actions.toggleGroupCheckbox({ + browserIds: group.browserIds, + checkStatus: getToggledCheckboxState(checkStatus) + }); + }; + + return ( +
+
+ + {name} +  ({`tests: ${testCount}, runs: ${resultCount}`})
- ); - } -} + {body} +
+ ); +}; + +GroupTestsItem.propTypes = { + group: PropTypes.object.isRequired, + isActive: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + checkStatus: PropTypes.number +}; + +export default connect( + ({tree}, {group}) => { + const childCount = group.browserIds.length; + const checkedCount = group.browserIds.reduce((sum, browserId) => { + return sum + tree.browsers.stateById[browserId].checkStatus; + }, 0); + const checkStatus = Number((checkedCount === childCount) || (checkedCount && INDETERMINATE)); + + return {checkStatus}; + }, + (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) +)(GroupTestsItem); diff --git a/lib/static/components/popup/index.js b/lib/static/components/popup/index.js index 1ba08cb16..62d10e086 100644 --- a/lib/static/components/popup/index.js +++ b/lib/static/components/popup/index.js @@ -9,7 +9,7 @@ import './index.styl'; const OVERFLOW_BORDER = 20; const Popup = (props) => { - const {action, target, children, ...rest} = props; + const {action, target, hideOnClick, children, ...rest} = props; const isClickHandled = useRef(false); const [show, setShow] = useState(false); @@ -41,6 +41,8 @@ const Popup = (props) => { const mouseClickOuterHandler = action === 'click' ? () => handleClick(true) : null; const mouseClickAwayHandler = action === 'click' ? () => handleClick(false, {isLast: true}) : null; + const childrenClickHandler = hideOnClick ? () => setShow(false) : null; + useEffect(() => { if (mouseClickAwayHandler) { window.addEventListener('click', mouseClickAwayHandler); @@ -69,6 +71,7 @@ const Popup = (props) => { ref={popupRef} className='popup__content' style={{transform: `translate(${-overflow}px)`}} + onClick={childrenClickHandler} > {children}
@@ -79,7 +82,8 @@ const Popup = (props) => { Popup.propTypes = { action: PropTypes.oneOf(['hover', 'click']).isRequired, - target: PropTypes.node.isRequired + target: PropTypes.node.isRequired, + hideOnClick: PropTypes.bool }; export default Popup; diff --git a/lib/static/components/popup/index.styl b/lib/static/components/popup/index.styl index 191e2732d..ebb62b770 100644 --- a/lib/static/components/popup/index.styl +++ b/lib/static/components/popup/index.styl @@ -1,37 +1,43 @@ -.popup - position absolute - z-index 1500 - transform translate(-50%, -1px) +.popup { + position: absolute; + z-index: 1500; + transform: translate(-50%, -1px); - &.popup_visible - visibility visible + &.popup_visible { + visibility: visible; + } - &.popup_hidden - visibility hidden + &.popup_hidden { + visibility: hidden; + } - .popup__pointer - position relative - height pointer-height = 20px + .popup__pointer { + position: relative; + height: pointer-height = 20px; - &::after - content '' - pointer-border-width = 1px - border-left pointer-border-width solid #ccc - border-top pointer-border-width solid #ccc - margin-top pointer-height - (2 * pointer-border-width * math(2, 'sqrt')) - position absolute - box-sizing content-box - height 20px - width 20px - background white - z-index 1400 - left 50% - transform rotate(45deg) translate(-50%) - - .popup__content - border 1px solid #ccc - border-radius .25rem - box-shadow 0 2px 4px 0 rgba(34, 36, 38, .12), - 0 2px 10px 0 rgba(34, 36, 38, .15) - background-color white - padding 15px + &::after { + content: ''; + pointer-border-width = 1px; + border-left: pointer-border-width solid #ccc; + border-top: pointer-border-width solid #ccc; + margin-top: pointer-height - (2 * pointer-border-width * math(2, 'sqrt')); + position: absolute; + box-sizing: content-box; + height: 20px; + width: 20px; + background: white; + z-index: 1400; + left: 50%; + transform: rotate(45deg) translate(-50%); + } + } + + .popup__content { + border: 1px solid #ccc; + border-radius: .25rem; + box-shadow: 0 2px 4px 0 rgba(34, 36, 38, .12), + 0 2px 10px 0 rgba(34, 36, 38, .15); + background-color: white; + padding: 15px; + } +} diff --git a/lib/static/components/section/section-browser.js b/lib/static/components/section/section-browser.js index b3b849271..755f55e89 100644 --- a/lib/static/components/section/section-browser.js +++ b/lib/static/components/section/section-browser.js @@ -79,7 +79,7 @@ class SectionBrowser extends Component { ); const section = isSkipped - ? + ? : ( { + const onToggleCheckbox = (e) => { + e.stopPropagation(); - render() { - const {title} = this.props; + props.actions.toggleBrowserCheckbox({ + suiteBrowserId: props.browserId, + checkStatus: getToggledCheckboxState(props.checkStatus) + }); + }; - return ( -
- {title} -
- ); - } -} + return ( +
+ + {props.title} +
+ ); +}; -export default BrowserSkippedTitle; +BrowserSkippedTitle.propTypes = { + title: PropTypes.object.isRequired, + browserId: PropTypes.string.isRequired, + // from store + checkStatus: PropTypes.number.isRequired +}; + +export default connect( + ({tree}, {browserId}) => ({checkStatus: tree.browsers.stateById[browserId].checkStatus}), + (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) +)(BrowserSkippedTitle); diff --git a/lib/static/components/section/title/browser.js b/lib/static/components/section/title/browser.js index 40f151d72..ab25e31eb 100644 --- a/lib/static/components/section/title/browser.js +++ b/lib/static/components/section/title/browser.js @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import React from 'react'; import ClipboardButton from 'react-clipboard.js'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; @@ -8,65 +8,72 @@ import * as actions from '../../../modules/actions'; import {appendQuery} from '../../../modules/query-params'; import viewModes from '../../../../constants/view-modes'; import {EXPAND_ALL} from '../../../../constants/expand-modes'; +import {getToggledCheckboxState} from '../../../../common-utils'; import ViewInBrowserIcon from '../../icons/view-in-browser'; +import Bullet from '../../bullet'; -class BrowserTitle extends Component { - static propTypes = { - title: PropTypes.node.isRequired, - browserId: PropTypes.string.isRequired, - browserName: PropTypes.string.isRequired, - lastResultId: PropTypes.string.isRequired, - handler: PropTypes.func.isRequired, - // from store - testName: PropTypes.string.isRequired, - retryIndex: PropTypes.number.isRequired, - suiteUrl: PropTypes.string - } - - _getTestUrl() { - const {browserName, testName, retryIndex} = this.props; - +const BrowserTitle = (props) => { + const getTestUrl = () => { return appendQuery(window.location.href, { - browser: browserName, - testNameFilter: testName, + browser: props.browserName, + testNameFilter: props.testName, strictMatchFilter: true, - retryIndex, + retryIndex: props.retryIndex, viewModes: viewModes.ALL, expand: EXPAND_ALL }); - } + }; + + const onCopyTestLink = (e) => { + e.stopPropagation(); + + props.actions.copyTestLink(); + }; - onCopyTestLink = (e) => { + const onToggleCheckbox = (e) => { e.stopPropagation(); - this.props.actions.copyTestLink(); - } + props.actions.toggleBrowserCheckbox({ + suiteBrowserId: props.browserId, + checkStatus: getToggledCheckboxState(props.checkStatus) + }); + }; - render() { - const {title, handler, lastResultId} = this.props; + return ( +
+ + {props.title} + + + +
+ ); +}; - return ( -
- {title} - - this._getTestUrl()}> - -
- ); - } -} +BrowserTitle.propTypes = { + title: PropTypes.node.isRequired, + browserId: PropTypes.string.isRequired, + browserName: PropTypes.string.isRequired, + lastResultId: PropTypes.string.isRequired, + handler: PropTypes.func.isRequired, + // from store + checkStatus: PropTypes.number.isRequired, + testName: PropTypes.string.isRequired, + retryIndex: PropTypes.number.isRequired, + suiteUrl: PropTypes.string +}; export default connect( ({tree}, {browserId}) => { - const browser = tree.browsers.byId[browserId]; const browserState = tree.browsers.stateById[browserId]; return { - testName: browser.parentId, + checkStatus: browserState.checkStatus, + testName: tree.browsers.byId[browserId].parentId, retryIndex: get(browserState, 'retryIndex', 0) }; }, diff --git a/lib/static/components/section/title/simple.js b/lib/static/components/section/title/simple.js index 616fbc2db..b3e1e2e40 100644 --- a/lib/static/components/section/title/simple.js +++ b/lib/static/components/section/title/simple.js @@ -1,82 +1,86 @@ 'use strict'; -import React, {Component} from 'react'; +import React from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import PropTypes from 'prop-types'; import ClipboardButton from 'react-clipboard.js'; import * as actions from '../../../modules/actions'; import {mkGetTestsBySuiteId} from '../../../modules/selectors/tree'; +import {getToggledCheckboxState} from '../../../../common-utils'; +import Bullet from '../../bullet'; -class SectionTitle extends Component { - static propTypes = { - name: PropTypes.string.isRequired, - suiteId: PropTypes.string.isRequired, - handler: PropTypes.func.isRequired, - // from store - gui: PropTypes.bool.isRequired, - suiteTests: PropTypes.arrayOf(PropTypes.shape({ - testName: PropTypes.string, - browserName: PropTypes.string - })).isRequired - } +const SectionTitle = ({name, suiteId, handler, gui, checkStatus, suiteTests, actions}) => { + const onCopySuiteName = (e) => { + e.stopPropagation(); + + actions.copySuiteName(suiteId); + }; - onCopySuiteName = (e) => { + const onSuiteRetry = (e) => { e.stopPropagation(); - this.props.actions.copySuiteName(this.props.suiteId); - } + actions.retrySuite(suiteTests); + }; - onSuiteRetry = (e) => { + const onToggleCheckbox = (e) => { e.stopPropagation(); - this.props.actions.retrySuite(this.props.suiteTests); - } + actions.toggleSuiteCheckbox({ + suiteId, + checkStatus: getToggledCheckboxState(checkStatus) + }); + }; - render() { - const {name, handler, gui} = this.props; + const drawCopyButton = () => ( + + + ); - return ( -
- {name} - {this._drawCopyButton()} - {gui && this._drawRetryButton()} -
- ); - } + const drawRetryButton = () => ( + + ); - _drawCopyButton() { - return ( - - - ); - } + return ( +
+ + {name} + {drawCopyButton()} + {gui && drawRetryButton()} +
+ ); +}; - _drawRetryButton() { - return ( - - ); - } -} +SectionTitle.propTypes = { + name: PropTypes.string.isRequired, + suiteId: PropTypes.string.isRequired, + handler: PropTypes.func.isRequired, + // from store + gui: PropTypes.bool.isRequired, + checkStatus: PropTypes.number, + suiteTests: PropTypes.arrayOf(PropTypes.shape({ + testName: PropTypes.string, + browserName: PropTypes.string + })).isRequired +}; export default connect( () => { const getTestsBySuiteId = mkGetTestsBySuiteId(); - return (state, {suiteId}) => { - return { - gui: state.gui, - suiteTests: getTestsBySuiteId(state, {suiteId}) - }; - }; + return (state, {suiteId}) => ({ + gui: state.gui, + checkStatus: state.tree.suites.stateById[suiteId].checkStatus, + suiteTests: getTestsBySuiteId(state, {suiteId}) + }); }, (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(SectionTitle); diff --git a/lib/static/gui.css b/lib/static/gui.css index e6cf8e704..b7e2b7daa 100644 --- a/lib/static/gui.css +++ b/lib/static/gui.css @@ -3,59 +3,79 @@ to {opacity: 0.3;} } -/* TODO: remove after header will be implemented */ -.report { - margin-top: 0; +.state-title:before, +.bullet_type-simple:before { + display: inline-block; + content: '\25cf'; + color: #ccc; + background-image: none; } -.section__title:before, .state-title:before { - display: inline-block; margin-right: 6px; vertical-align: middle; - content: '\25cf'; - color: #ccc; - width: auto; height: auto; - background-image: none; +} + +.bullet_type-simple:before { + text-align: end; + line-height: 18px; + width: 18px; + height: 100%; } .custom-icon_retry:before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath transform='scale(0.027,-0.027) translate(0,-448)' d=' M500.333 448H452.922C446.069 448 440.608 442.271 440.9360000000001 435.426L444.9020000000001 352.6670000000001C399.416 406.101 331.6720000000001 440 256.001 440C119.34 440 7.899 328.474 8 191.813C8.101 54.932 119.096 -56 256 -56C319.926 -56 378.202 -31.813 422.178 7.908C427.291 12.526 427.532 20.469 422.6600000000001 25.341L388.689 59.312C384.223 63.778 377.0490000000001 64.029 372.3090000000001 59.855C341.308 32.552 300.606 16 256 16C158.733 16 80 94.716 80 192C80 289.267 158.716 368 256 368C316.892 368 370.506 337.142 402.099 290.2L300.574 295.065C293.729 295.3930000000001 288 289.932 288 283.079V235.668C288 229.041 293.373 223.668 300 223.668H500.333C506.96 223.668 512.333 229.041 512.333 235.668V436C512.333 442.627 506.96 448 500.333 448z'/%3E%3C/svg%3E"); } -.section_status_success > .section__title:before, +.section_status_success > .section__title .bullet_type-simple:before, .state-title_success:before { color: #038035; } -.section_status_fail > .section__title:before, -.section_status_error > .section__title:before, +.section_status_fail > .section__title .bullet_type-simple:before, +.section_status_error > .section__title .bullet_type-simple:before, .state-title_fail:before, .state-title_error:before { color: #c00; } -.section_status_skipped > .section__title:before, +.section_status_skipped > .section__title .bullet_type-simple:before, .state-title_skipped:before { color: #ccc; } -.section_status_queued > .section__title::before { - color: #e7d700; +.section_status_idle > .section__title .ui.checkbox label:before, +.section_status_idle > .section__title .ui.checkbox label:after, +.tests-group > .tests-group__title .ui.checkbox label:before, +.tests-group > .tests-group__title .ui.checkbox label:after { + color: black !important; + border-color: black !important; +} + +.section_status_queued > .section__title .bullet_type-simple:before +.section_status_queued > .section__title .ui.checkbox label:before, +.section_status_queued > .section__title .ui.checkbox label:after { + color: #e7d700 !important; + border-color: #e7d700 !important; } -.section_status_running > .section__title::before { +.section_status_running > .section__title .bullet_type-simple:before, +.section_status_running > .section__title .ui.checkbox label:before, +.section_status_running > .section__title .ui.checkbox label:after { -webkit-animation: blink 0.5s linear 0s infinite alternate; animation: blink 0.5s linear 0s infinite alternate; - color: #e7d700; + color: #e7d700 !important; + border-color: #e7d700 !important; } .section_status_updated > .section__title, -.section_status_updated > .section__title::before, -.state-title_updated:before { - color: #038035; +.section_status_updated > .section__title .bullet_type-simple::before, +.section_status_updated > .section__title .ui.checkbox label:before, +.section_status_updated > .section__title .ui.checkbox label:after { + color: #038035 !important; + border-color: #038035 !important; } .report_show-only-errors .section_status_updated { @@ -101,10 +121,6 @@ animation: blink 0.5s linear 0s infinite alternate; } -.section_collapsed .section__title:before { - transform: none; -} - .ui.modal > .content { width: auto !important; } diff --git a/lib/static/hooks/useLocalStorage.js b/lib/static/hooks/useLocalStorage.js new file mode 100644 index 000000000..4d5e59731 --- /dev/null +++ b/lib/static/hooks/useLocalStorage.js @@ -0,0 +1,39 @@ +import {useCallback, useEffect, useState} from 'react'; +import useEventListener from './useEventListener'; +import * as localStorageWrapper from '../modules/local-storage-wrapper'; + +export default function useLocalStorage(key, initialValue) { + const readValue = useCallback(() => { + return localStorageWrapper.getItem(key, initialValue); + }, [key, initialValue]); + + const [storedValue, setStoredValue] = useState(readValue); + + const writeValue = useCallback(newValue => { + const customEvent = Object.assign(new Event('local-storage'), {key}); + const settingValue = newValue instanceof Function + ? newValue(storedValue) + : newValue; + + localStorageWrapper.setItem(key, settingValue); + + setStoredValue(settingValue); + + window.dispatchEvent(customEvent); + }, []); + + useEffect(() => { + setStoredValue(readValue()); + }, []); + + const handleStorageChange = useCallback((event) => { + if (event && event.key === key) { + setStoredValue(readValue()); + } + }, [key, readValue]); + + useEventListener('storage', handleStorageChange); + useEventListener('local-storage', handleStorageChange); + + return [storedValue, writeValue]; +} diff --git a/lib/static/modules/action-names.js b/lib/static/modules/action-names.js index 61fcc713c..4d1e6d3f4 100644 --- a/lib/static/modules/action-names.js +++ b/lib/static/modules/action-names.js @@ -52,6 +52,9 @@ export default { TOGGLE_META_INFO: 'TOGGLE_META_INFO', TOGGLE_PAGE_SCREENSHOT: 'TOGGLE_PAGE_SCREENSHOT', TOGGLE_TESTS_GROUP: 'TOGGLE_TESTS_GROUP', + TOGGLE_SUITE_CHECKBOX: 'TOGGLE_SUITE_CHECKBOX', + TOGGLE_GROUP_CHECKBOX: 'TOGGLE_GROUP_CHECKBOX', UPDATE_BOTTOM_PROGRESS_BAR: 'UPDATE_BOTTOM_PROGRESS_BAR', - GROUP_TESTS_BY_KEY: 'GROUP_TESTS_BY_KEY' + GROUP_TESTS_BY_KEY: 'GROUP_TESTS_BY_KEY', + TOGGLE_BROWSER_CHECKBOX: 'TOGGLE_BROWSER_CHECKBOX' }; diff --git a/lib/static/modules/actions.js b/lib/static/modules/actions.js index 34ca94376..b4f65d3c2 100644 --- a/lib/static/modules/actions.js +++ b/lib/static/modules/actions.js @@ -189,7 +189,7 @@ export const stopTests = () => async dispatch => { await axios.post('/stop'); dispatch({type: actionNames.STOP_TESTS}); } catch (e) { - console.error(`Error while stopping tests: {e}`); + console.error('Error while stopping tests:', e); } }; @@ -231,6 +231,9 @@ export const toggleSuiteSection = (payload) => ({type: actionNames.TOGGLE_SUITE_ export const toggleBrowserSection = (payload) => ({type: actionNames.TOGGLE_BROWSER_SECTION, payload}); export const toggleMetaInfo = () => ({type: actionNames.TOGGLE_META_INFO}); export const togglePageScreenshot = () => ({type: actionNames.TOGGLE_PAGE_SCREENSHOT}); +export const toggleBrowserCheckbox = (payload) => ({type: actionNames.TOGGLE_BROWSER_CHECKBOX, payload}); +export const toggleSuiteCheckbox = (payload) => ({type: actionNames.TOGGLE_SUITE_CHECKBOX, payload}); +export const toggleGroupCheckbox = (payload) => ({type: actionNames.TOGGLE_GROUP_CHECKBOX, payload}); export const updateBottomProgressBar = (payload) => ({type: actionNames.UPDATE_BOTTOM_PROGRESS_BAR, payload}); export const toggleTestsGroup = (payload) => ({type: actionNames.TOGGLE_TESTS_GROUP, payload}); export const groupTestsByKey = (payload) => ({type: actionNames.GROUP_TESTS_BY_KEY, payload}); diff --git a/lib/static/modules/reducers/tree/helpers.js b/lib/static/modules/reducers/tree/helpers.js index 224f0c627..84e02845d 100644 --- a/lib/static/modules/reducers/tree/helpers.js +++ b/lib/static/modules/reducers/tree/helpers.js @@ -26,3 +26,19 @@ export function shouldNodeBeOpened(expand, {errorsCb, retriesCb}) { return false; } + +export function getShownCheckedChildCount(tree, suiteId) { + const {suiteIds = [], browserIds = []} = tree.suites.byId[suiteId]; + const checkedChildBrowserCount = browserIds.reduce((sum, browserChildId) => { + const browserState = tree.browsers.stateById[browserChildId]; + + return sum + (browserState.shouldBeShown && browserState.checkStatus); + }, 0); + const checkedChildSuitesCount = suiteIds.reduce((sum, suiteChildId) => { + const suiteState = tree.suites.stateById[suiteChildId]; + + return sum + (suiteState.shouldBeShown && suiteState.checkStatus); + }, 0); + + return checkedChildBrowserCount + checkedChildSuitesCount; +} diff --git a/lib/static/modules/reducers/tree/index.js b/lib/static/modules/reducers/tree/index.js index 08a68c8ef..3187a8d0f 100644 --- a/lib/static/modules/reducers/tree/index.js +++ b/lib/static/modules/reducers/tree/index.js @@ -2,11 +2,11 @@ import {findLast} from 'lodash'; import {produce} from 'immer'; import actionNames from '../../action-names'; import { - initSuitesState, changeAllSuitesState, changeSuiteState, updateSuitesStatus, - getFailedRootSuiteIds, updateAllSuitesStatus, calcSuitesShowness, calcSuitesOpenness, failSuites + initSuitesState, changeAllSuitesState, changeSuiteState, updateSuitesStatus, getFailedRootSuiteIds, + updateAllSuitesStatus, calcSuitesShowness, calcSuitesOpenness, failSuites, updateParentsChecked } from './nodes/suites'; import { - initBrowsersState, changeAllBrowsersState, changeBrowserState, + initBrowsersState, changeAllBrowsersState, changeBrowserState, getBrowserParentId, calcBrowsersShowness, calcBrowsersOpenness, setBrowsersLastRetry } from './nodes/browsers'; import {initResultsState, changeAllResultsState, changeResultState, addResult, removeResult} from './nodes/results'; @@ -268,6 +268,61 @@ export default produce((state, action) => { break; } + + case actionNames.TOGGLE_BROWSER_CHECKBOX: { + const {suiteBrowserId, checkStatus} = action.payload; + const parentId = getBrowserParentId(state.tree, suiteBrowserId); + + changeBrowserState(state.tree, suiteBrowserId, {checkStatus}); + + updateParentsChecked(state.tree, parentId); + + break; + } + + case actionNames.TOGGLE_SUITE_CHECKBOX: { + const {suiteId, checkStatus} = action.payload; + const parentId = state.tree.suites.byId[suiteId].parentId; + const toggledSuiteIds = [suiteId]; + + changeSuiteState(state.tree, suiteId, {checkStatus}); + + while (toggledSuiteIds.length) { + const suiteCurrId = toggledSuiteIds.pop(); + const suiteChildIds = state.tree.suites.byId[suiteCurrId].suiteIds || []; + const suiteBrowserIds = state.tree.suites.byId[suiteCurrId].browserIds || []; + + suiteChildIds.forEach(suiteChildId => { + const isSuiteShown = state.tree.suites.stateById[suiteChildId].shouldBeShown; + const newCheckStatus = Number(isSuiteShown && checkStatus); + changeSuiteState(state.tree, suiteChildId, {checkStatus: newCheckStatus}); + }); + suiteBrowserIds.forEach(suiteBrowserId => { + const isBrowserShown = state.tree.browsers.stateById[suiteBrowserId].shouldBeShown; + const newCheckStatus = Number(isBrowserShown && checkStatus); + changeBrowserState(state.tree, suiteBrowserId, {checkStatus: newCheckStatus}); + }); + + toggledSuiteIds.push(...suiteChildIds); + } + + updateParentsChecked(state.tree, parentId); + + break; + } + + case actionNames.TOGGLE_GROUP_CHECKBOX: { + const {browserIds, checkStatus} = action.payload; + const parentIds = browserIds.map(browserId => getBrowserParentId(state.tree, browserId)); + + browserIds.forEach(browserId => { + changeBrowserState(state.tree, browserId, {checkStatus}); + }); + + updateParentsChecked(state.tree, parentIds); + + break; + } } }); diff --git a/lib/static/modules/reducers/tree/nodes/browsers.js b/lib/static/modules/reducers/tree/nodes/browsers.js index e08a12c28..d66e33bef 100644 --- a/lib/static/modules/reducers/tree/nodes/browsers.js +++ b/lib/static/modules/reducers/tree/nodes/browsers.js @@ -1,11 +1,13 @@ import {isEmpty, last, initial} from 'lodash'; import {isBrowserMatchViewMode, isTestNameMatchFilters, shouldShowBrowser} from '../../../utils'; +import {UNCHECKED} from '../../../../../constants/checked-statuses'; import {isNodeFailed} from '../../../utils'; import {changeNodeState, shouldNodeBeOpened} from '../helpers'; export function initBrowsersState(tree, view) { tree.browsers.allIds.forEach((browserId) => { setBrowsersLastRetry(tree, browserId); + changeBrowserState(tree, browserId, {checkStatus: UNCHECKED}); if (view.keyToGroupTestsBy) { changeBrowserState(tree, browserId, {shouldBeShown: false}); @@ -17,6 +19,10 @@ export function initBrowsersState(tree, view) { }); } +export function getBrowserParentId(tree, browserId) { + return tree.browsers.byId[browserId].parentId; +} + export function changeAllBrowsersState(tree, state) { tree.browsers.allIds.forEach((browserId) => { changeBrowserState(tree, browserId, state); @@ -47,8 +53,9 @@ export function calcBrowsersShowness(tree, view, browserIds) { const browser = tree.browsers.byId[browserId]; const lastResult = tree.results.byId[last(browser.resultIds)]; const shouldBeShown = calcBrowserShowness(browser, lastResult, view); + const checkStatus = shouldBeShown && tree.browsers.stateById[browserId].checkStatus; - changeBrowserState(tree, browserId, {shouldBeShown}); + changeBrowserState(tree, browserId, {shouldBeShown, checkStatus}); }); } diff --git a/lib/static/modules/reducers/tree/nodes/suites.js b/lib/static/modules/reducers/tree/nodes/suites.js index 24ecb3ca8..1eadb4cbc 100644 --- a/lib/static/modules/reducers/tree/nodes/suites.js +++ b/lib/static/modules/reducers/tree/nodes/suites.js @@ -2,11 +2,14 @@ import _ from 'lodash'; import {getSuiteResults} from '../../../selectors/tree'; import {isNodeFailed} from '../../../utils'; import {determineStatus, isFailStatus} from '../../../../../common-utils'; -import {changeNodeState, shouldNodeBeOpened} from '../helpers'; +import {changeNodeState, getShownCheckedChildCount, shouldNodeBeOpened} from '../helpers'; import {EXPAND_RETRIES} from '../../../../../constants/expand-modes'; import {FAIL} from '../../../../../constants/test-statuses'; +import {INDETERMINATE, UNCHECKED} from '../../../../../constants/checked-statuses'; export function initSuitesState(tree, view) { + changeAllSuitesState(tree, {checkStatus: UNCHECKED}); + if (view.keyToGroupTestsBy) { changeAllSuitesState(tree, {shouldBeShown: false}); } else { @@ -34,6 +37,20 @@ export function updateSuitesStatus(tree, suites) { }); } +export function updateParentsChecked(tree, parentIds) { + const youngerSuites = [].concat(parentIds) + .filter(parentId => parentId) + .map((suiteId) => tree.suites.byId[suiteId]); + + const changeParentSuiteCb = (parentSuite) => { + changeSuiteState(tree, parentSuite.id, {checkStatus: shouldSuiteBeChecked(parentSuite, tree)}); + }; + + youngerSuites.forEach(changeParentSuiteCb); + + calcParentSuitesState(youngerSuites, tree, changeParentSuiteCb); +} + export function getFailedRootSuiteIds(suites) { return suites.allRootIds.filter((rootId) => { return isNodeFailed(suites.byId[rootId]); @@ -57,12 +74,16 @@ export function calcSuitesShowness(tree, suiteIds) { youngestSuites.forEach((suite) => { const shouldBeShown = suite.browserIds .some((browserId) => tree.browsers.stateById[browserId].shouldBeShown); + const checkStatus = shouldSuiteBeChecked(suite, tree); - changeSuiteState(tree, suite.id, {shouldBeShown}); + changeSuiteState(tree, suite.id, {shouldBeShown, checkStatus}); }); const changeParentSuiteCb = (parentSuite) => { - changeSuiteState(tree, parentSuite.id, {shouldBeShown: shouldSuiteBeShown(parentSuite, tree)}); + changeSuiteState(tree, parentSuite.id, { + shouldBeShown: shouldSuiteBeShown(parentSuite, tree), + checkStatus: shouldSuiteBeChecked(parentSuite, tree) + }); }; calcParentSuitesState(youngestSuites, tree, changeParentSuiteCb); @@ -185,6 +206,17 @@ function shouldSuiteBeOpened(suite, tree) { return shouldSuiteBe(suite, tree, 'shouldBeOpened'); } +function shouldSuiteBeChecked(suite, tree) { + const shownChildSuiteCount = (tree.suites.byId[suite.id].suiteIds || []) + .reduce((count, childSuiteId) => count + tree.suites.stateById[childSuiteId].shouldBeShown, 0); + const shownChildBrowserCount = (tree.suites.byId[suite.id].browserIds || []) + .reduce((count, childBrowserId) => count + tree.browsers.stateById[childBrowserId].shouldBeShown, 0); + const childCount = shownChildSuiteCount + shownChildBrowserCount; + const checkedChildCount = getShownCheckedChildCount(tree, suite.id); + + return Number((checkedChildCount === childCount) || (checkedChildCount && INDETERMINATE)); +} + function shouldSuiteBe(suite, tree, field) { return (suite.suiteIds || []).some((suiteId) => tree.suites.stateById[suiteId][field]) || (suite.browserIds || []).some((browserId) => tree.browsers.stateById[browserId][field]); @@ -224,7 +256,7 @@ function getChildSuitesStatus(tree, suite, filteredBrowsers) { const suiteBrowsers = suite.browserIds .map((id) => tree.browsers.byId[id]) .filter(({name, version}) => { - var res = filteredBrowsers.some(({id: filteredName, versions: filteredVersions}) => { + const res = filteredBrowsers.some(({id: filteredName, versions: filteredVersions}) => { return filteredName === name && (filteredVersions.includes(version) || !filteredVersions.length); }); diff --git a/lib/static/modules/selectors/tree.js b/lib/static/modules/selectors/tree.js index dc3ea2b3c..2cd753bee 100644 --- a/lib/static/modules/selectors/tree.js +++ b/lib/static/modules/selectors/tree.js @@ -8,6 +8,7 @@ import {isNodeFailed, isNodeSuccessful, isAcceptable, iterateSuites} from '../ut const getSuites = (state) => state.tree.suites.byId; const getSuitesStates = (state) => state.tree.suites.stateById; const getBrowsers = (state) => state.tree.browsers.byId; +const getBrowserIds = (state) => state.tree.browsers.allIds; const getBrowsersStates = (state) => state.tree.browsers.stateById; const getResults = (state) => state.tree.results.byId; const getImages = (state) => state.tree.images.byId; @@ -51,6 +52,18 @@ export const getFailedTests = createSelector( } ); +export const getCheckedTests = createSelector( + getBrowserIds, getBrowsers, getBrowsersStates, + (browserIds, browsers, browsersStates) => { + const checkedBrowsers = browserIds.filter((browserId) => browsersStates[browserId].checkStatus); + + return checkedBrowsers.map((browserId) => ({ + testName: browsers[browserId].parentId, + browserName: browsers[browserId].name + })); + } +); + export const getAcceptableImagesByStateName = createSelector( getBrowsersStates, getBrowsers, getResults, getImages, (browsersStates, browsers, results, images) => { diff --git a/lib/static/styles.css b/lib/static/styles.css index 8b230b125..baa5f43e1 100644 --- a/lib/static/styles.css +++ b/lib/static/styles.css @@ -52,25 +52,25 @@ main.container { color: #000; } -.strict-match-filter { +.toggle-control { display: inline-block; height: 25px; line-height: 21px; } -.strict-match-filter .ui.checkbox label { +.toggle-control .ui.checkbox label { padding-left: 55px; } -.strict-match-filter .ui.toggle.checkbox input:focus:checked~.box:before, -.strict-match-filter .ui.toggle.checkbox input:focus:checked~label:before, -.strict-match-filter .ui.toggle.checkbox input:checked~.box:before, -.strict-match-filter .ui.toggle.checkbox input:checked~label:before { +.toggle-control .ui.toggle.checkbox input:focus:checked~.box:before, +.toggle-control .ui.toggle.checkbox input:focus:checked~label:before, +.toggle-control .ui.toggle.checkbox input:checked~.box:before, +.toggle-control .ui.toggle.checkbox input:checked~label:before { background-color: #ffeb9f!important; } -.strict-match-filter .ui.checkbox .box, -.strict-match-filter .ui.checkbox label { +.toggle-control .ui.checkbox .box, +.toggle-control .ui.checkbox label { font-size: 11px; font-family: inherit; } @@ -275,6 +275,34 @@ main.container { user-select: none; } +.ui.checkbox.indeterminate label:after { + margin-top: 0.5px; +} + +.section__title .ui.checkbox, +.tests-group__title .ui.checkbox { + vertical-align: bottom; + margin-right: 5px; + + width: 18px; + height: 18px; +} + +.section__title .ui.checkbox label:before, +.tests-group__title .ui.checkbox label:before { + width: 14px; + height: 14px; + border-width: 1.5px; +} + +.section__title .ui.checkbox label:after, +.tests-group__title .ui.checkbox label:after { + width: 14px; + height: 14px; + font-size: 14px; + line-height: 14px; +} + .section__body .section__title { font-size: 15px; line-height: 20px; @@ -284,14 +312,14 @@ main.container { font-weight: normal; } -.section__title:before, -.tests-group__title:before, +.section__title .bullet_type-simple:before, +.tests-group__title .bullet_type-simple.tests-group__bullet:before, .state-title:after, .details__summary:after, .error .details__summary:before, .sticky-header__wrap:before { content: ''; - width: 16px; + width: 18px; height: 16px; display: inline-block; margin-right: 5px; @@ -317,7 +345,7 @@ main.container { background-image: url("data:image/svg+xml,%3Csvg width='16px' height='16px' viewBox='0 0 64 64' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M35 0 C31 0 27 2 24 5 C22 7 21 8 20 11 C18 15 18 22 19 27 C20 29 21 31 22 33 C23 34 25 36 27 37 C27 37 28 37 28 37 C28 37 26 42 23 48 C20 55 17 60 17 62 L16 64 L20 63 L24 63 L35 39 L38 39 L41 39 L41 64 L47 64 L47 0 L42 0 C38 0 35 0 35 0 ZM41 19 L41 33 L38 33 C36 33 34 33 34 33 C31 32 28 30 27 27 C25 23 26 16 27 12 C28 9 31 6 34 6 C35 6 35 5 35 5 C36 5 37 5 38 5 L41 5 ZM41 19'/%3E%3C/svg%3E%0A"); } -.tests-group__title:before { +.tests-group__title .bullet_type-simple.tests-group__bullet:before { flex-shrink: 0; } @@ -347,8 +375,8 @@ details[open] > .details__summary:before { content: none; } -.section_collapsed .section__title:before, -.tests-group_collapsed .tests-group__title:before, +.section_collapsed .section__title .bullet_type-simple:before, +.tests-group_collapsed .tests-group__title .tests-group__bullet:before, .state-title_collapsed.state-title:after { transform: rotate(180deg); } @@ -357,6 +385,12 @@ details[open] > .details__summary:before { color: #8c8c8c; } +.section_status_skipped > .section__title .ui.checkbox label:before, +.section_status_skipped > .section__title .ui.checkbox label:after { + color: #8c8c8c !important; + border-color: #8c8c8c !important; +} + .section_status_skipped > .section__title:hover { color: #c6c6c6; } @@ -384,6 +418,14 @@ details[open] > .details__summary:before { color: #038035; } +.section_status_success > .section__title .ui.checkbox label:before, +.section_status_success > .section__title .ui.checkbox label:after, +.section_status_updated > .section__title .ui.checkbox label:before, +.section_status_updated > .section__title .ui.checkbox label:after { + color: #038035 !important; + border-color: #038035 !important; +} + .section_status_success > .section__title:hover, .state-title_success:hover, .state-title_updated:hover { @@ -397,6 +439,14 @@ details[open] > .details__summary:before { color: #c00; } +.section_status_fail > .section__title .ui.checkbox label:before, +.section_status_fail > .section__title .ui.checkbox label:after, +.section_status_error > .section__title .ui.checkbox label:before, +.section_status_error > .section__title .ui.checkbox label:after { + color: #c00 !important; + border-color: #c00 !important; +} + .section_status_fail > .section__title:hover, .section_status_error > .section__title:hover, .state-title_fail:hover, diff --git a/test/unit/lib/static/components/bullet.js b/test/unit/lib/static/components/bullet.js new file mode 100644 index 000000000..0d35ca6bc --- /dev/null +++ b/test/unit/lib/static/components/bullet.js @@ -0,0 +1,68 @@ +import React from 'react'; +import proxyquire from 'proxyquire'; +import {CHECKED, UNCHECKED, INDETERMINATE} from 'lib/constants/checked-statuses'; + +describe('', () => { + const sandbox = sinon.sandbox.create(); + let Bullet, useLocalStorageStub; + + beforeEach(() => { + useLocalStorageStub = sandbox.stub(); + Bullet = proxyquire('lib/static/components/bullet', { + '../hooks/useLocalStorage': {default: useLocalStorageStub} + }).default; + }); + + afterEach(() => sandbox.restore()); + + it('should render simple bullet if checkboxes are disabled', () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([false]); + + const component = mount(); + + assert.isTrue(component.find('.bullet_type-simple').exists()); + }); + + it('should render checkbox bullet if checkboxes are enabled', () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([true]); + + const component = mount(); + + assert.isTrue(component.find('.bullet_type-checkbox').exists()); + }); + + describe('', () => { + beforeEach(() => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([true]); + }); + + it('should be "checked" if status is CHECKED', () => { + const component = mount(); + + assert.isTrue(component.find('.checkbox.checked').exists()); + }); + + it('should be "indeterminate" if status is INDETERMINATE', () => { + const component = mount(); + + assert.isTrue(component.find('.checkbox.indeterminate').exists()); + }); + + it('should be "unchecked" if status is UNCHECKED', () => { + const component = mount(); + + assert.isTrue(component.find('.checkbox').exists()); + assert.isFalse(component.find('.checkbox').hasClass('checked')); + assert.isFalse(component.find('.checkbox').hasClass('indeterminate')); + }); + + it('should call "onClick" callback', () => { + const onClickStub = sandbox.stub(); + const component = mount(); + + component.find('.checkbox').simulate('click'); + + assert.calledOnce(onClickStub); + }); + }); +}); diff --git a/test/unit/lib/static/components/controls/gui-controls.js b/test/unit/lib/static/components/controls/gui-controls.js index 4c5c206ec..f28d75102 100644 --- a/test/unit/lib/static/components/controls/gui-controls.js +++ b/test/unit/lib/static/components/controls/gui-controls.js @@ -1,117 +1,39 @@ import React from 'react'; -import RunButton from 'lib/static/components/controls/run-button'; import proxyquire from 'proxyquire'; -import {mkState, mkConnectedComponent} from '../utils'; +import {mkConnectedComponent} from '../utils'; describe('', () => { const sandbox = sinon.sandbox.create(); - let GuiControls, AcceptOpenedButton, CommonControls, actionsStub, selectors; + let GuiControls, RunButton, AcceptOpenedButton, CommonControls, CommonFilters, actionsStub, selectors; beforeEach(() => { + RunButton = sandbox.stub().returns(null); AcceptOpenedButton = sandbox.stub().returns(null); CommonControls = sandbox.stub().returns(null); + CommonFilters = sandbox.stub().returns(null); actionsStub = { runAllTests: sandbox.stub().returns({type: 'some-type'}), runFailedTests: sandbox.stub().returns({type: 'some-type'}), stopTests: sandbox.stub().returns({type: 'some-type'}) }; selectors = { - getFailedTests: sandbox.stub().returns([]) + getFailedTests: sandbox.stub().returns([]), + getCheckedTests: sandbox.stub().returns([]) }; GuiControls = proxyquire('lib/static/components/controls/gui-controls', { + './run-button': {default: RunButton}, './accept-opened-button': {default: AcceptOpenedButton}, './common-controls': {default: CommonControls}, + './common-filters': {default: CommonFilters}, '../../modules/actions': actionsStub, - '../../modules/selectors/tree': selectors + '../../../modules/selectors/tree': selectors }).default; }); afterEach(() => sandbox.restore()); - describe('"Run" button', () => { - it('should be disabled if no suites to run', () => { - const component = mkConnectedComponent(, { - initialState: {tree: {suites: {allRootIds: []}}, processing: false} - }); - - assert.isTrue(component.find(RunButton).prop('isDisabled')); - }); - - it('should be enabled if suites exist to run', () => { - const component = mkConnectedComponent(, { - initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} - }); - - assert.isFalse(component.find(RunButton).prop('isDisabled')); - }); - - it('should be disabled while processing something', () => { - const component = mkConnectedComponent(, { - initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: true} - }); - - assert.isTrue(component.find(RunButton).prop('isDisabled')); - }); - - it('should pass "autoRun" prop', () => { - const component = mkConnectedComponent(, { - initialState: {autoRun: true} - }); - - assert.isTrue(component.find(RunButton).prop('autoRun')); - assert.calledOnce(actionsStub.runAllTests); - }); - - it('should call "runAllTests" action on click', () => { - const component = mkConnectedComponent(, { - initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} - }); - - component.find(RunButton).simulate('click'); - - assert.calledOnce(actionsStub.runAllTests); - }); - }); - - describe('"Retry failed tests" button', () => { - it('should be disabled if no failed suites to run', () => { - const component = mkConnectedComponent(, { - initialState: {tree: {suites: {failedRootIds: []}}, processing: false} - }); - - assert.isTrue(component.find('[label="Retry failed tests"]').prop('isDisabled')); - }); - - it('should be enabled if failed suites exist to run', () => { - const component = mkConnectedComponent(, { - initialState: {tree: {suites: {failedRootIds: ['suite']}}, processing: false} - }); - - assert.isFalse(component.find('[label="Retry failed tests"]').prop('isDisabled')); - }); - - it('should be disabled while processing something', () => { - const component = mkConnectedComponent(, { - initialState: {tree: {suites: {failedRootIds: ['suite']}}, processing: true} - }); - - assert.isTrue(component.find('[label="Retry failed tests"]').prop('isDisabled')); - }); - - it('should call "runFailedTests" action on click', () => { - const failedTests = [{testName: 'suite test', browserName: 'yabro'}]; - const state = mkState({initialState: {tree: {suites: {failedRootIds: ['suite']}}, processing: false}}); - selectors.getFailedTests.withArgs(state).returns(failedTests); - const component = mkConnectedComponent(, {state}); - - component.find('[label="Retry failed tests"]').simulate('click'); - - assert.calledOnceWith(actionsStub.runFailedTests, failedTests); - }); - }); - describe('"Accept opened" button', () => { it('should render button', () => { mkConnectedComponent(); diff --git a/test/unit/lib/static/components/controls/run-button.js b/test/unit/lib/static/components/controls/run-button.js new file mode 100644 index 000000000..4557cde22 --- /dev/null +++ b/test/unit/lib/static/components/controls/run-button.js @@ -0,0 +1,208 @@ +import React from 'react'; +import proxyquire from 'proxyquire'; +import {mkConnectedComponent, mkState} from '../utils'; + +describe('', () => { + const sandbox = sinon.sandbox.create(); + + let RunButton, useLocalStorageStub, actionsStub, selectorsStub; + + beforeEach(() => { + useLocalStorageStub = sandbox.stub().returns([true]); + actionsStub = { + runAllTests: sandbox.stub().returns({type: 'some-type'}), + runFailedTests: sandbox.stub().returns({type: 'some-type'}), + retrySuite: sandbox.stub().returns({type: 'some-type'}) + }; + selectorsStub = { + getFailedTests: sandbox.stub().returns([]), + getCheckedTests: sandbox.stub().returns([]) + }; + + RunButton = proxyquire('lib/static/components/controls/run-button', { + '../../../hooks/useLocalStorage': {default: useLocalStorageStub}, + '../../../modules/selectors/tree': selectorsStub, + '../../../modules/actions': actionsStub + }).default; + }); + + it('should be disabled if no suites to run', () => { + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootIds: []}}, processing: false} + }); + + assert.isTrue(component.find('button').prop('disabled')); + }); + + it('should be enabled if suites exist to run', () => { + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} + }); + + assert.isFalse(component.find('button').prop('disabled')); + }); + + it('should be disabled while processing something', () => { + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: true} + }); + + assert.isTrue(component.find('button').prop('disabled')); + }); + + it('should run all tests with "autoRun" prop', () => { + mkConnectedComponent(, { + initialState: {autoRun: true} + }); + + assert.calledOnce(actionsStub.runAllTests); + }); + + it('should call "runAllTests" action on "Run all tests" click', () => { + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} + }); + + component.find('button').simulate('click'); + + assert.calledOnce(actionsStub.runAllTests); + }); + + it('should call "runFailedTests" action on "Run failed tests" click', () => { + const failedTests = [{testName: 'suite test', browserName: 'yabro'}]; + const state = mkState({initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false}}); + selectorsStub.getFailedTests.withArgs(state).returns(failedTests); + const component = mkConnectedComponent(, {state}); + component.find({children: 'Failed'}).simulate('click'); + + component.find('button').simulate('click'); + + assert.calledOnceWith(actionsStub.runFailedTests, failedTests); + }); + + it('should call "retrySuite" action on "Run checked tests" click', () => { + const checkedTests = [{testName: 'suite test', browserName: 'yabro'}]; + const state = mkState({initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false}}); + selectorsStub.getCheckedTests.withArgs(state).returns(checkedTests); + const component = mkConnectedComponent(, {state}); + component.find({children: 'Checked'}).simulate('click'); + + component.find('button').simulate('click'); + + assert.calledOnceWith(actionsStub.retrySuite, checkedTests); + }); + + describe('Label', () => { + it('should be "Running" if is running', () => { + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false, running: true} + }); + + assert.equal(component.find('button').text(), 'Running'); + }); + + it('should be "Run all tests" by default if there is no checked tests', () => { + selectorsStub.getCheckedTests.returns([]); + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} + }); + + assert.equal(component.find('button').text(), 'Run all tests'); + }); + + it('should be "Run checked tests" if there are checked tests', () => { + selectorsStub.getCheckedTests.returns([{testName: 'testName', browserName: 'browserName'}]); + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} + }); + + assert.equal(component.find('button').text(), 'Run checked tests'); + }); + + it('should be "Run failed tests" if picked', () => { + selectorsStub.getFailedTests.returns([{testName: 'testName', browserName: 'browserName'}]); + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} + }); + + component.find({children: 'Failed'}).simulate('click'); + assert.equal(component.find('button').text(), 'Run failed tests'); + }); + + it('should be "Run checked tests" if picked', () => { + selectorsStub.getCheckedTests.returns([{testName: 'testName', browserName: 'browserName'}]); + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} + }); + + component.find({children: 'Checked'}).simulate('click'); + assert.equal(component.find('button').text(), 'Run checked tests'); + }); + }); + + describe('Popup', () => { + describe('should be hidden', () => { + it('if processing', () => { + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootSuiteIds: ['suite']}}, processing: true} + }); + + assert.isFalse(component.find('.run-mode').exists()); + }); + + it('if no suites', () => { + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootSuiteIds: []}}, processing: false} + }); + + assert.isFalse(component.find('.run-mode').exists()); + }); + + it('if there are no checked and failed tests', () => { + selectorsStub.getCheckedTests.returns([]); + selectorsStub.getFailedTests.returns([]); + + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootSuiteIds: ['suite']}}, processing: false} + }); + + assert.isFalse(component.find('.run-mode').exists()); + }); + + it('if checkboxes are hidden and no failed tests', () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([false]); + selectorsStub.getCheckedTests.returns([{testName: 'testName', browserName: 'browserName'}]); + selectorsStub.getFailedTests.returns([]); + + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootSuiteIds: ['suite']}}, processing: false} + }); + + assert.isFalse(component.find('.run-mode').exists()); + }); + }); + + describe('should be shown', () => { + it('if failed suites exist', () => { + selectorsStub.getFailedTests.returns([{testName: 'testName', browserName: 'browserName'}]); + + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootSuiteIds: ['suite']}}, processing: false} + }); + + assert.isFalse(component.find('.run-mode').exists()); + }); + + it('if checked suites exist and checkboxes are shown', () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([true]); + selectorsStub.getCheckedTests.returns([{testName: 'testName', browserName: 'browserName'}]); + + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootSuiteIds: ['suite']}}, processing: false} + }); + + assert.isFalse(component.find('.run-mode').exists()); + }); + }); + }); +}); diff --git a/test/unit/lib/static/components/controls/show-checkboxes-input.js b/test/unit/lib/static/components/controls/show-checkboxes-input.js new file mode 100644 index 000000000..cd4c5ef51 --- /dev/null +++ b/test/unit/lib/static/components/controls/show-checkboxes-input.js @@ -0,0 +1,40 @@ +import React from 'react'; +import proxyquire from 'proxyquire'; +import {Checkbox} from 'semantic-ui-react'; + +describe('', () => { + const sandbox = sinon.sandbox.create(); + + let ShowCheckboxesInput; + let useLocalStorageStub; + + beforeEach(() => { + useLocalStorageStub = sandbox.stub(); + + ShowCheckboxesInput = proxyquire('lib/static/components/controls/show-checkboxes-input', { + '../../hooks/useLocalStorage': {default: useLocalStorageStub} + }).default; + }); + + afterEach(() => sandbox.restore()); + + [true, false].forEach(checked => { + it(`should set checkbox to ${checked ? '' : 'un'}checked if showCheckboxes is ${checked ? '' : 'not '}set`, () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([checked, () => {}]); + + const component = mount(); + + assert.equal(component.find('input[type="checkbox"]').prop('checked'), checked); + }); + + it(`should call hook handler on ${checked ? '' : 'un'}checked click`, () => { + const hookHandler = sandbox.stub(); + useLocalStorageStub.withArgs('showCheckboxes', false).returns([checked, hookHandler]); + const component = mount(); + + component.find(Checkbox).simulate('change'); + + assert.calledOnceWith(hookHandler, !checked); + }); + }); +}); diff --git a/test/unit/lib/static/components/group-tests/item.js b/test/unit/lib/static/components/group-tests/item.js new file mode 100644 index 000000000..6ce94c4dd --- /dev/null +++ b/test/unit/lib/static/components/group-tests/item.js @@ -0,0 +1,95 @@ +import React from 'react'; +import proxyquire from 'proxyquire'; +import {defaultsDeep, set} from 'lodash'; +import {Checkbox} from 'semantic-ui-react'; +import {CHECKED, UNCHECKED, INDETERMINATE} from 'lib/constants/checked-statuses'; +import {isCheckboxChecked} from 'lib/common-utils.js'; +import {mkConnectedComponent} from 'test/unit/lib/static/components/utils'; +import {mkStateTree} from 'test/unit/lib/static/state-utils'; + +describe('', () => { + const sandbox = sinon.sandbox.create(); + let GroupTestsItem, actionsStub, useLocalStorageStub, SuitesStub; + + const mkGroupTestsItemComponent = (browserIds = [], browsersStateById = {}) => { + const props = { + isActive: false, + onClick: sandbox.stub(), + group: {browserIds} + }; + const initialState = defaultsDeep( + set({}, 'tree.browsers.stateById', browsersStateById), + set({}, 'tree', mkStateTree) + ); + + return mkConnectedComponent(, {initialState}); + }; + + beforeEach(() => { + actionsStub = {toggleGroupCheckbox: sandbox.stub().returns({type: 'some-type'})}; + useLocalStorageStub = sandbox.stub().returns([false]); + SuitesStub = sandbox.stub().returns(null); + + GroupTestsItem = proxyquire('lib/static/components/group-tests/item', { + '../../modules/actions': actionsStub, + './suites': {default: SuitesStub}, + '../bullet': proxyquire('lib/static/components/bullet', { + '../hooks/useLocalStorage': {default: useLocalStorageStub} + }) + }).default; + }); + + describe('', () => { + [true, false].forEach(show => { + it(`should ${show ? '' : 'not '}exist if "showCheckboxes" is ${show ? '' : 'not '}set`, () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([show]); + + const component = mkGroupTestsItemComponent(); + + assert.equal(component.find(Checkbox).exists(), show); + }); + }); + + it('should not be checked when no childs checked', () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([true]); + const browserIds = ['b1']; + const browsersStateById = {'b1': {shouldBeChecked: UNCHECKED}}; + const component = mkGroupTestsItemComponent(browserIds, browsersStateById); + + assert.equal(component.find(Checkbox).prop('checked'), UNCHECKED); + }); + + [ + {checked: INDETERMINATE, state: 'indeterminate', childChecked: 'some'}, + {checked: CHECKED, state: 'checked', childChecked: 'all'} + ].forEach(({checked, state, childChecked}) => { + it(`should be ${state} when ${childChecked} child checked`, () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([true]); + const browserIds = ['b1', 'b2']; + const browsersStateById = { + 'b1': {checkStatus: CHECKED}, + 'b2': {checkStatus: checked === CHECKED ? CHECKED : UNCHECKED} + }; + const component = mkGroupTestsItemComponent(browserIds, browsersStateById); + + assert.equal(component.find(Checkbox).prop('checked'), isCheckboxChecked(checked)); + }); + }); + + [CHECKED, UNCHECKED].forEach(checked => { + it(`should call "toggleBrowserCheckbox" action with ${checked ? 'un' : ''}checked state on click`, () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([true]); + const browserIds = ['b1']; + const browsersStateById = {'b1': {checkStatus: checked}}; + const component = mkGroupTestsItemComponent(browserIds, browsersStateById); + + component.find(Checkbox).simulate('click'); + + assert.calledOnceWith(actionsStub.toggleGroupCheckbox, { + browserIds: ['b1'], + checkStatus: checked ? UNCHECKED : CHECKED + }); + }); + }); + }); +}); diff --git a/test/unit/lib/static/components/section/section-browser.js b/test/unit/lib/static/components/section/section-browser.js index 5bd28f4ab..5de7d95e8 100644 --- a/test/unit/lib/static/components/section/section-browser.js +++ b/test/unit/lib/static/components/section/section-browser.js @@ -1,31 +1,37 @@ import React from 'react'; -import {defaults} from 'lodash'; +import {defaults, set} from 'lodash'; import proxyquire from 'proxyquire'; import {SUCCESS, SKIPPED, ERROR} from 'lib/constants/test-statuses'; +import {UNCHECKED} from 'lib/constants/checked-statuses'; import {mkConnectedComponent} from '../utils'; import {mkBrowser, mkResult, mkStateTree} from '../../state-utils'; describe('', () => { const sandbox = sinon.sandbox.create(); - let SectionBrowser, Body, actionsStub; + let SectionBrowser, Body, BrowserTitle, BrowserSkippedTitle, actionsStub; const mkSectionBrowserComponent = (props = {}, initialState = {}) => { props = defaults(props, { browserId: 'default-bro-id' }); initialState = defaults(initialState, { - tree: mkStateTree() + tree: mkStateTree(), + checkStatus: UNCHECKED }); return mkConnectedComponent(, {initialState}); }; beforeEach(() => { + BrowserSkippedTitle = sandbox.stub().returns(null); + BrowserTitle = sandbox.stub().returns(null); Body = sandbox.stub().returns(null); actionsStub = {toggleBrowserSection: sandbox.stub().returns({type: 'some-type'})}; SectionBrowser = proxyquire('lib/static/components/section/section-browser', { '../../modules/actions': actionsStub, + './title/browser-skipped': {default: BrowserSkippedTitle}, + './title/browser': {default: BrowserTitle}, './body': {default: Body} }).default; }); @@ -33,28 +39,31 @@ describe('', () => { afterEach(() => sandbox.restore()); describe('skipped test', () => { - it('should render "[skipped]" tag in title', () => { + it('should pass "[skipped]" tag in title', () => { const browsersById = mkBrowser({id: 'yabro-1', name: 'yabro', resultIds: ['res'], parentId: 'test'}); const browsersStateById = {'yabro-1': {shouldBeShown: true, shouldBeOpened: false}}; const resultsById = mkResult({id: 'res', status: SKIPPED}); const tree = mkStateTree({browsersById, browsersStateById, resultsById}); - const component = mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); + mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); - assert.equal(component.find('.section__title_skipped').first().text(), `[${SKIPPED}] yabro`); + assert.calledWithMatch( + BrowserSkippedTitle, + set({}, 'title.props.children', [`[${SKIPPED}] `, 'yabro', undefined, undefined]) + ); }); - it('should render skip reason', () => { + it('should pass skip reason', () => { const browsersById = mkBrowser({id: 'yabro-1', name: 'yabro', resultIds: ['res'], parentId: 'test'}); const browsersStateById = {'yabro-1': {shouldBeShown: true, shouldBeOpened: false}}; const resultsById = mkResult({id: 'res', status: SKIPPED, skipReason: 'some-reason'}); const tree = mkStateTree({browsersById, browsersStateById, resultsById}); - const component = mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); + mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); - assert.equal( - component.find('.section__title_skipped').first().text(), - `[${SKIPPED}] yabro, reason: some-reason` + assert.calledWithMatch( + BrowserSkippedTitle, + set({}, 'title.props.children', [`[${SKIPPED}] `, 'yabro', ', reason: ', 'some-reason']) ); }); @@ -80,13 +89,14 @@ describe('', () => { ...mkResult({id: 'res-1', status: ERROR, error: {}}), ...mkResult({id: 'res-2', status: SKIPPED, skipReason: 'some-reason'}) }; - const tree = mkStateTree({browsersById, browsersStateById, resultsById}); - const component = mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); + mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); - assert.equal(component.find('.section__title').first().text(), `[${SKIPPED}] yabro, reason: some-reason`); - assert.isFalse(component.find('.section__title').exists('.section__title_skipped')); + assert.calledWithMatch( + BrowserTitle, + set({}, 'title.props.children', [`[${SKIPPED}] `, 'yabro', ', reason: ', 'some-reason']) + ); }); it('should render body if browser in opened state', () => { @@ -140,44 +150,17 @@ describe('', () => { }); }); - describe('"toggleBrowserSection" action', () => { - it('should call on click in browser title of not skipped test', () => { - const browsersById = mkBrowser({id: 'yabro-1', name: 'yabro', resultIds: ['res-1'], parentId: 'test'}); - const browsersStateById = {'yabro-1': {shouldBeShown: true, shouldBeOpened: false}}; - const resultsById = mkResult({id: 'res-1', status: SUCCESS}); - - const tree = mkStateTree({browsersById, browsersStateById, resultsById}); - - const component = mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); - component.find('.section__title').simulate('click'); - - assert.calledOnceWith(actionsStub.toggleBrowserSection, {browserId: 'yabro-1', shouldBeOpened: true}); - }); - }); - - describe('', () => { - let BrowserTitle; + it('should render "BrowserTitle" with "browserName" for correctly working clipboard button', () => { + const browsersById = mkBrowser({id: 'yabro', name: 'yabro', resultIds: ['res']}); + const browsersStateById = {'yabro': {shouldBeShown: true, shouldBeOpened: false}}; + const resultsById = mkResult({id: 'res', status: SUCCESS}); + const tree = mkStateTree({browsersById, browsersStateById, resultsById}); - beforeEach(() => { - BrowserTitle = sandbox.stub().returns(null); - SectionBrowser = proxyquire('lib/static/components/section/section-browser', { - './title/browser': {default: BrowserTitle} - }).default; - }); - - it('should render "BrowserTitle" with "browserName" for correctly working clipboard button', () => { - const browsersById = mkBrowser({id: 'yabro', name: 'yabro', resultIds: ['res']}); - const browsersStateById = {'yabro': {shouldBeShown: true, shouldBeOpened: false}}; - const resultsById = mkResult({id: 'res', status: SUCCESS}); - const tree = mkStateTree({browsersById, browsersStateById, resultsById}); + mkSectionBrowserComponent({browserId: 'yabro'}, {tree}); - mkSectionBrowserComponent({browserId: 'yabro'}, {tree}); - - assert.calledOnceWith(BrowserTitle, { - browserId: 'yabro', browserName: 'yabro', handler: sinon.match.any, - lastResultId: 'res', title: 'yabro' - }); + assert.calledOnceWith(BrowserTitle, { + browserId: 'yabro', browserName: 'yabro', handler: sinon.match.any, + lastResultId: 'res', title: 'yabro' }); }); }); - diff --git a/test/unit/lib/static/components/section/title/browser-skipped.js b/test/unit/lib/static/components/section/title/browser-skipped.js new file mode 100644 index 000000000..c47f5f358 --- /dev/null +++ b/test/unit/lib/static/components/section/title/browser-skipped.js @@ -0,0 +1,71 @@ +import React from 'react'; +import {defaults} from 'lodash'; +import proxyquire from 'proxyquire'; +import {mkConnectedComponent} from 'test/unit/lib/static/components/utils'; +import {mkBrowser, mkResult, mkStateTree} from 'test/unit/lib/static/state-utils'; +import {SKIPPED} from 'lib/constants/test-statuses'; +import {CHECKED, UNCHECKED} from 'lib/constants/checked-statuses'; + +describe('', () => { + const sandbox = sinon.sandbox.create(); + let BrowserSkippedTitle, useLocalStorageStub, actionsStub; + + const mkBrowserSkippedTitleComponent = (props = {}, initialState = {}) => { + props = defaults(props, { + title: 'default_title' + }); + initialState = defaults(initialState, { + tree: mkStateTree() + }); + + return mkConnectedComponent(, {initialState}); + }; + + beforeEach(() => { + useLocalStorageStub = sandbox.stub().returns([false]); + actionsStub = { + toggleBrowserCheckbox: sandbox.stub().returns({type: 'some-type'}) + }; + + BrowserSkippedTitle = proxyquire('lib/static/components/section/title/browser-skipped', { + '../../../modules/actions': actionsStub, + '../../bullet': proxyquire('lib/static/components/bullet', { + '../hooks/useLocalStorage': {default: useLocalStorageStub} + }) + }).default; + }); + + describe('', () => { + [true, false].forEach(show => { + it(`should ${show ? '' : 'not '}exist if "showCheckboxes" is ${show ? '' : 'not '}set`, () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([show]); + const browsersById = mkBrowser({id: 'yabro', name: 'yabro', resultIds: ['default_res']}); + const resultsById = mkResult({id: 'default_res', status: SKIPPED, skipReason: 'some-reason'}); + const browsersStateById = {'yabro': {checkStatus: UNCHECKED}}; + const tree = mkStateTree({browsersById, resultsById, browsersStateById}); + + const component = mkBrowserSkippedTitleComponent({browserId: 'yabro'}, {tree}); + + assert.equal(component.find('.checkbox').exists(), show); + }); + }); + + [CHECKED, UNCHECKED].forEach(checked => { + it(`should call "toggleBrowserCheckbox" action with ${checked ? 'unchecked' : 'checked'} stat on click`, () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([true]); + const browsersById = mkBrowser({id: 'yabro', name: 'yabro', resultIds: ['default_res']}); + const resultsById = mkResult({id: 'default_res', status: SKIPPED, skipReason: 'some-reason'}); + const browsersStateById = {'yabro': {checkStatus: checked}}; + const tree = mkStateTree({browsersById, resultsById, browsersStateById}); + const component = mkBrowserSkippedTitleComponent({browserId: 'yabro'}, {tree}); + + component.find('.checkbox').simulate('click'); + + assert.calledOnceWith(actionsStub.toggleBrowserCheckbox, { + suiteBrowserId: 'yabro', + checkStatus: checked ? UNCHECKED : CHECKED + }); + }); + }); + }); +}); diff --git a/test/unit/lib/static/components/section/title/browser.js b/test/unit/lib/static/components/section/title/browser.js index 962f57043..eb61e34f8 100644 --- a/test/unit/lib/static/components/section/title/browser.js +++ b/test/unit/lib/static/components/section/title/browser.js @@ -3,14 +3,14 @@ import {defaults} from 'lodash'; import proxyquire from 'proxyquire'; import {mkConnectedComponent} from 'test/unit/lib/static/components/utils'; import {mkBrowser, mkResult, mkStateTree} from 'test/unit/lib/static/state-utils'; -import {SKIPPED} from 'lib/constants/test-statuses'; -import actionNames from 'lib/static/modules/action-names'; +import {SUCCESS} from 'lib/constants/test-statuses'; import viewModes from 'lib/constants/view-modes'; import {EXPAND_ALL} from 'lib/constants/expand-modes'; +import {CHECKED, UNCHECKED} from 'lib/constants/checked-statuses'; describe('', () => { const sandbox = sinon.sandbox.create(); - let BrowserTitle, actionsStub, queryParams; + let BrowserTitle, actionsStub, queryParams, useLocalStorageStub, ViewInBrowserIcon; const mkBrowserTitleComponent = (props = {}, initialState = {}) => { props = defaults(props, { @@ -29,24 +29,66 @@ describe('', () => { beforeEach(() => { actionsStub = { - copyTestLink: sandbox.stub().returns({type: actionNames.COPY_TEST_LINK}) + toggleBrowserCheckbox: sandbox.stub().returns({type: 'some-type'}), + copyTestLink: sandbox.stub().returns({type: 'some-type'}) }; queryParams = { appendQuery: sandbox.stub().returns(null) }; + useLocalStorageStub = sandbox.stub().returns([false]); + ViewInBrowserIcon = sandbox.stub().returns(null); BrowserTitle = proxyquire('lib/static/components/section/title/browser', { '../../../modules/actions': actionsStub, - '../../../modules/query-params': queryParams + '../../../modules/query-params': queryParams, + '../../icons/view-in-browser': {default: ViewInBrowserIcon}, + '../../bullet': proxyquire('lib/static/components/bullet', { + '../hooks/useLocalStorage': {default: useLocalStorageStub} + }) }).default; }); + describe('', () => { + [true, false].forEach(show => { + it(`should ${show ? '' : 'not '}exist if "showCheckboxes" is ${show ? '' : 'not '}set`, () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([show]); + const browsersById = mkBrowser({id: 'yabro', name: 'yabro', resultIds: ['default_res']}); + const resultsById = mkResult({id: 'default_res', status: SUCCESS, skipReason: 'some-reason'}); + const browsersStateById = {'yabro': {checkStatus: UNCHECKED}}; + const tree = mkStateTree({browsersById, resultsById, browsersStateById}); + + const component = mkBrowserTitleComponent({browserId: 'yabro'}, {tree}); + + assert.equal(component.find('.checkbox').exists(), show); + }); + }); + + [CHECKED, UNCHECKED].forEach(checked => { + it(`should call "toggleBrowserCheckbox" action with ${checked ? 'unchecked' : 'checked'} state on click`, () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([true]); + const browsersById = mkBrowser({id: 'yabro', name: 'yabro', resultIds: ['default_res']}); + const resultsById = mkResult({id: 'default_res', status: SUCCESS, skipReason: 'some-reason'}); + const browsersStateById = {'yabro': {checkStatus: checked}}; + const tree = mkStateTree({browsersById, resultsById, browsersStateById}); + const component = mkBrowserTitleComponent({browserId: 'yabro'}, {tree}); + + component.find('.checkbox').simulate('click'); + + assert.calledOnceWith(actionsStub.toggleBrowserCheckbox, { + suiteBrowserId: 'yabro', + checkStatus: checked ? UNCHECKED : CHECKED + }); + }); + }); + }); + describe('', () => { it('should call action "onCopyTestLink" on click', () => { const browsersById = mkBrowser({id: 'yabro', name: 'yabro', resultIds: ['default_res']}); - const resultsById = mkResult({id: 'default_res', status: SKIPPED, skipReason: 'some-reason'}); - const tree = mkStateTree({browsersById, resultsById}); + const resultsById = mkResult({id: 'default_res', status: SUCCESS, skipReason: 'some-reason'}); + const browsersStateById = {'yabro': {checkStatus: UNCHECKED}}; + const tree = mkStateTree({browsersById, resultsById, browsersStateById}); const component = mkBrowserTitleComponent({browserId: 'yabro'}, {tree}); component.find('ClipboardButton').simulate('click'); @@ -57,8 +99,9 @@ describe('', () => { it('should call "appendQuery" with correct arguments', () => { const browsersById = mkBrowser({id: 'yabro', name: 'yabro', parentId: 'test'}); - const resultsById = mkResult({id: 'default_res', status: SKIPPED, skipReason: 'some-reason'}); - const tree = mkStateTree({browsersById, resultsById}); + const resultsById = mkResult({id: 'default_res', status: SUCCESS, skipReason: 'some-reason'}); + const browsersStateById = {'yabro': {checkStatus: UNCHECKED}}; + const tree = mkStateTree({browsersById, resultsById, browsersStateById}); const component = mkBrowserTitleComponent({browserId: 'yabro', browserName: 'yabro'}, {tree}); @@ -76,4 +119,17 @@ describe('', () => { }); }); }); + + it('should call "toggleBrowserSection" action on click in browser title', () => { + const handler = sandbox.stub(); + const browsersById = mkBrowser({id: 'yabro', name: 'yabro', resultIds: ['res-1'], parentId: 'test'}); + const browsersStateById = {'yabro': {shouldBeShown: true, shouldBeOpened: false}}; + const resultsById = mkResult({id: 'res-1', status: SUCCESS}); + const tree = mkStateTree({browsersById, browsersStateById, resultsById}); + const component = mkBrowserTitleComponent({browserId: 'yabro', browserName: 'yabro', handler}, {tree}); + + component.find('.section__title').simulate('click'); + + assert.calledOnce(handler); + }); }); diff --git a/test/unit/lib/static/components/section/title/simple.js b/test/unit/lib/static/components/section/title/simple.js new file mode 100644 index 000000000..7d2dc279a --- /dev/null +++ b/test/unit/lib/static/components/section/title/simple.js @@ -0,0 +1,88 @@ +import React from 'react'; +import proxyquire from 'proxyquire'; +import {defaultsDeep, set} from 'lodash'; +import {Checkbox} from 'semantic-ui-react'; +import {mkConnectedComponent} from 'test/unit/lib/static/components/utils'; +import {mkStateTree} from 'test/unit/lib/static/state-utils'; +import {CHECKED, UNCHECKED, INDETERMINATE} from 'lib/constants/checked-statuses'; + +describe('', () => { + const sandbox = sinon.sandbox.create(); + let SuiteTitle, actionsStub, useLocalStorageStub, mkGetTestsBySuiteIdStub; + + const mkSuiteTitleComponent = (checkStatus) => { + const props = { + name: 'suiteName', + suiteId: 'suiteId', + handler: sandbox.stub() + }; + const initialState = defaultsDeep( + set({}, 'tree.suites.stateById.suiteId.checkStatus', checkStatus), + set({}, 'tree', mkStateTree), + set({}, 'gui', true) + ); + + return mkConnectedComponent(, {initialState}); + }; + + beforeEach(() => { + actionsStub = {toggleSuiteCheckbox: sandbox.stub().returns({type: 'some-type'})}; + useLocalStorageStub = sandbox.stub().returns([false]); + mkGetTestsBySuiteIdStub = sandbox.stub().returns(sandbox.stub().returns([])); + + SuiteTitle = proxyquire('lib/static/components/section/title/simple', { + '../../../modules/actions': actionsStub, + '../../../modules/selectors/tree': {mkGetTestsBySuiteId: mkGetTestsBySuiteIdStub}, + '../../bullet': proxyquire('lib/static/components/bullet', { + '../hooks/useLocalStorage': {default: useLocalStorageStub} + }) + }).default; + }); + + describe('', () => { + [CHECKED, UNCHECKED].forEach(show => { + it(`should ${show ? '' : 'not '}exist if "showCheckboxes" is ${show ? '' : 'not '}set`, () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([show]); + + const component = mkSuiteTitleComponent(); + + assert.equal(component.find(Checkbox).exists(), +show); + }); + }); + + it('should not be checked', () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([true]); + const component = mkSuiteTitleComponent(UNCHECKED); + + assert.equal(component.find(Checkbox).prop('checked'), UNCHECKED); + }); + + it(`should be indeterminate`, () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([true]); + const component = mkSuiteTitleComponent(INDETERMINATE); + + assert.isTrue(component.find(Checkbox).prop('indeterminate')); + }); + + it(`should be checked`, () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([true]); + const component = mkSuiteTitleComponent(CHECKED); + + assert.equal(component.find(Checkbox).prop('checked'), CHECKED); + }); + + [CHECKED, UNCHECKED].forEach(checked => { + it(`should call "toggleBrowserCheckbox" action with ${checked ? 'unchecked' : 'checked'} state on click`, () => { + useLocalStorageStub.withArgs('showCheckboxes', false).returns([true]); + const component = mkSuiteTitleComponent(checked); + + component.find(Checkbox).simulate('click'); + + assert.calledOnceWith(actionsStub.toggleSuiteCheckbox, { + suiteId: 'suiteId', + checkStatus: checked ? UNCHECKED : CHECKED + }); + }); + }); + }); +}); diff --git a/test/unit/lib/static/modules/reducers/tree/index.js b/test/unit/lib/static/modules/reducers/tree/index.js index 5f996dcb0..d878539e6 100644 --- a/test/unit/lib/static/modules/reducers/tree/index.js +++ b/test/unit/lib/static/modules/reducers/tree/index.js @@ -1,4 +1,5 @@ import {SUCCESS, FAIL, ERROR, UPDATED} from 'lib/constants/test-statuses'; +import {CHECKED, UNCHECKED, INDETERMINATE} from 'lib/constants/checked-statuses'; import reducer from 'lib/static/modules/reducers/tree'; import actionNames from 'lib/static/modules/action-names'; import viewModes from 'lib/constants/view-modes'; @@ -86,8 +87,8 @@ describe('lib/static/modules/reducers/tree', () => { payload: {tree} }); - assert.isTrue(newState.tree.suites.stateById.s1.shouldBeShown); - assert.isTrue(newState.tree.suites.stateById.s2.shouldBeShown); + assert.equal(newState.tree.suites.stateById.s1.shouldBeShown, CHECKED); + assert.equal(newState.tree.suites.stateById.s2.shouldBeShown, CHECKED); }); it('test name is matched on test filter', () => { @@ -104,8 +105,8 @@ describe('lib/static/modules/reducers/tree', () => { payload: {tree} }); - assert.isTrue(newState.tree.suites.stateById.s1.shouldBeShown); - assert.isTrue(newState.tree.suites.stateById.s2.shouldBeShown); + assert.equal(newState.tree.suites.stateById.s1.shouldBeShown, CHECKED); + assert.equal(newState.tree.suites.stateById.s2.shouldBeShown, CHECKED); }); it('child browser is shown by view mode but browsers from child suites not', () => { @@ -303,6 +304,33 @@ describe('lib/static/modules/reducers/tree', () => { }); }); }); + + describe('"checkStatus"', () => { + it('should be unchecked', () => { + const suitesById = { + ...mkSuite({id: 's1', suiteIds: ['s2'], status: SUCCESS}), + ...mkSuite({id: 's2', browserIds: ['b1', 'b2'], parentId: 's1', status: SUCCESS}) + }; + const browsersById = { + ...mkBrowser({id: 'b1', parentId: 's2', resultIds: ['r1']}), + ...mkBrowser({id: 'b2', parentId: 's2', resultIds: ['r2']}) + }; + const resultsById = { + ...mkResult({id: 'r1', status: SUCCESS}), + ...mkResult({id: 'r2', status: SUCCESS}) + }; + const tree = mkStateTree({suitesById, browsersById, resultsById}); + const view = mkStateView(); + + const newState = reducer({view}, { + type: actionName, + payload: {tree} + }); + + assert.equal(newState.tree.suites.stateById.s1.checkStatus, UNCHECKED); + assert.equal(newState.tree.suites.stateById.s2.checkStatus, UNCHECKED); + }); + }); }); describe('init browsers states with', () => { @@ -554,6 +582,33 @@ describe('lib/static/modules/reducers/tree', () => { }); }); }); + + describe('"checkStatus"', () => { + it('should be unchecked', () => { + const suitesById = { + ...mkSuite({id: 's1', suiteIds: ['s2']}), + ...mkSuite({id: 's2', browserIds: ['b1', 'b2'], parentId: 's1'}) + }; + const browsersById = { + ...mkBrowser({id: 'b1', parentId: 's2', resultIds: ['r1']}), + ...mkBrowser({id: 'b2', parentId: 's2', resultIds: ['r2']}) + }; + const resultsById = { + ...mkResult({id: 'r1', status: SUCCESS}), + ...mkResult({id: 'r2', status: SUCCESS}) + }; + const tree = mkStateTree({suitesById, browsersById, resultsById}); + const view = mkStateView(); + + const newState = reducer({view}, { + type: actionName, + payload: {tree} + }); + + assert.equal(newState.tree.browsers.stateById.b1.checkStatus, UNCHECKED); + assert.equal(newState.tree.browsers.stateById.b2.checkStatus, UNCHECKED); + }); + }); }); describe('init images states with "shouldBeOpened"', () => { @@ -888,7 +943,8 @@ describe('lib/static/modules/reducers/tree', () => { }; const browsersById = mkBrowser({id: 'b1', name: 'yabro', parentId: 's2', resultIds: ['r1']}); const resultsById = mkResult({id: 'r1', parentId: 'b1', status: ERROR}); - const tree = mkStateTree({suitesById, browsersById, resultsById}); + const browsersStateById = {'b1': {checkStatus: UNCHECKED}}; + const tree = mkStateTree({suitesById, browsersById, resultsById, browsersStateById}); const filteredBrowsers = [{id: 'yabro', versions: []}]; const view = mkStateView({filteredBrowsers}); @@ -915,7 +971,8 @@ describe('lib/static/modules/reducers/tree', () => { ...mkResult({id: 'r1', parentId: 'b1', status: FAIL}), ...mkResult({id: 'r2', parentId: 'b2', status: ERROR}) }; - const tree = mkStateTree({suitesById, browsersById, resultsById}); + const browsersStateById = {'b1': {checkStatus: UNCHECKED}}; + const tree = mkStateTree({suitesById, browsersById, resultsById, browsersStateById}); const filteredBrowsers = [{id: 'yabro-1', versions: []}]; const view = mkStateView({filteredBrowsers}); @@ -1056,7 +1113,8 @@ describe('lib/static/modules/reducers/tree', () => { const browsersById = {...mkBrowser({id: 'b1', resultIds: ['r1']})}; const resultsById = {...mkResult({id: 'r1', status: SUCCESS})}; const resultsStateById = {r1: {}, r2: {}}; - const tree = mkStateTree({browsersById, resultsById, resultsStateById}); + const browsersStateById = {'b1': {checkStatus: UNCHECKED}}; + const tree = mkStateTree({browsersById, resultsById, resultsStateById, browsersStateById}); const newState = reducer({tree, view: mkStateView()}, { type: actionNames.TOGGLE_TESTS_GROUP, @@ -1072,7 +1130,8 @@ describe('lib/static/modules/reducers/tree', () => { const browsersById = {...mkBrowser({id: 'b1', resultIds: ['r1', 'r2']})}; const resultsById = {...mkResult({id: 'r1', status: SUCCESS}), ...mkResult({id: 'r2', status: SUCCESS})}; const resultsStateById = {r1: {}, r2: {}}; - const tree = mkStateTree({browsersById, resultsById, resultsStateById}); + const browsersStateById = {'b1': {checkStatus: UNCHECKED}}; + const tree = mkStateTree({browsersById, resultsById, resultsStateById, browsersStateById}); const newState = reducer({tree, view: mkStateView()}, { type: actionNames.TOGGLE_TESTS_GROUP, @@ -1091,7 +1150,8 @@ describe('lib/static/modules/reducers/tree', () => { ...mkResult({id: 'r2', status: SUCCESS}), ...mkResult({id: 'r3', status: SUCCESS}) }; - const tree = mkStateTree({browsersById, resultsById}); + const browsersStateById = {'b1': {checkStatus: UNCHECKED}}; + const tree = mkStateTree({browsersById, resultsById, browsersStateById}); const newState = reducer({tree, view: mkStateView()}, { type: actionNames.TOGGLE_TESTS_GROUP, @@ -1289,4 +1349,381 @@ describe('lib/static/modules/reducers/tree', () => { assert.equal(state, newState); }); }); + + describe(`${actionNames.TOGGLE_BROWSER_CHECKBOX} action`, () => { + [CHECKED, UNCHECKED].forEach(checkStatus => { + it(`should set browser checkbox to ${checkStatus ? '' : 'un'}checked state`, () => { + const suitesById = mkSuite({id: 's1', browserIds: ['b1']}); + const browsersById = mkBrowser({id: 'b1', parentId: 's1'}); + const suitesStateById = {s1: {shouldBeShown: true, checkStatus: Number(!checkStatus)}}; + const browsersStateById = {b1: {shouldBeShown: true, checkStatus: Number(!checkStatus)}}; + const tree = mkStateTree({suitesById, suitesStateById, browsersById, browsersStateById}); + const view = mkStateView({keyToGroupTestsBy: ''}); + + const newState = reducer({tree, view}, { + type: actionNames.TOGGLE_BROWSER_CHECKBOX, + payload: {suiteBrowserId: 'b1', checkStatus} + }); + + assert.equal(newState.tree.browsers.stateById.b1.checkStatus, checkStatus); + }); + + it(`should not change ${checkStatus ? '' : 'un'}checked state`, () => { + const suitesById = mkSuite({id: 's1', browserIds: ['b1']}); + const browsersById = mkBrowser({id: 'b1', parentId: 's1'}); + const suitesStateById = {s1: {shouldBeShown: true, checkStatus}}; + const browsersStateById = {b1: {shouldBeShown: true, checkStatus}}; + const tree = mkStateTree({suitesById, suitesStateById, browsersById, browsersStateById}); + const view = mkStateView({keyToGroupTestsBy: ''}); + + const newState = reducer({tree, view}, { + type: actionNames.TOGGLE_BROWSER_CHECKBOX, + payload: {suiteBrowserId: 'b1', checkStatus} + }); + + assert.equal(newState.tree.browsers.stateById.b1.checkStatus, checkStatus); + }); + + it(`should update parents to ${checkStatus ? '' : 'un'}checked state`, () => { + const suitesById = { + ...mkSuite({id: 's0', suiteIds: ['s1']}), + ...mkSuite({id: 's1', browserIds: ['b1'], parentId: 's0'}) + }; + const browsersById = mkBrowser({id: 'b1', parentId: 's1'}); + const suitesStateById = { + s0: {shouldBeShown: true, checkStatus: Number(!checkStatus)}, + s1: {shouldBeShown: true, checkStatus: Number(!checkStatus)} + }; + const browsersStateById = {b1: {shouldBeShown: true, checkStatus: Number(!checkStatus)}}; + const tree = mkStateTree({suitesById, suitesStateById, browsersById, browsersStateById}); + const view = mkStateView({keyToGroupTestsBy: ''}); + + const newState = reducer({tree, view}, { + type: actionNames.TOGGLE_BROWSER_CHECKBOX, + payload: {suiteBrowserId: 'b1', checkStatus} + }); + + assert.equal(newState.tree.suites.stateById.s1.checkStatus, checkStatus); + assert.equal(newState.tree.suites.stateById.s0.checkStatus, checkStatus); + }); + + it(`should update parents to indeterminate from ${checkStatus ? '' : 'un'}checked state`, () => { + const suitesById = { + ...mkSuite({id: 's0', suiteIds: ['s1']}), + ...mkSuite({id: 's1', browserIds: ['b1', 'b2'], parentId: 's0'}) + }; + const browsersById = { + ...mkBrowser({id: 'b1', parentId: 's1'}), + ...mkBrowser({id: 'b2', parentId: 's1'}) + }; + const suitesStateById = { + s0: {shouldBeShown: true, checkStatus}, + s1: {shouldBeShown: true, checkStatus} + }; + const browsersStateById = { + b1: {shouldBeShown: true, checkStatus}, + b2: {shouldBeShown: true, checkStatus} + }; + const tree = mkStateTree({suitesById, suitesStateById, browsersById, browsersStateById}); + const view = mkStateView({keyToGroupTestsBy: ''}); + + const newState = reducer({tree, view}, { + type: actionNames.TOGGLE_BROWSER_CHECKBOX, + payload: {suiteBrowserId: 'b1', checkStatus: Number(!checkStatus)} + }); + + assert.equal(newState.tree.suites.stateById.s1.checkStatus, INDETERMINATE); + assert.equal(newState.tree.suites.stateById.s0.checkStatus, INDETERMINATE); + }); + }); + }); + + describe(`${actionNames.TOGGLE_SUITE_CHECKBOX} action`, () => { + [CHECKED, UNCHECKED].forEach(checkStatus => { + it(`should set suite checkbox to ${checkStatus ? '' : 'un'}checked state`, () => { + const suitesById = mkSuite({id: 's1', browserIds: ['b1']}); + const browsersById = mkBrowser({id: 'b1', parentId: 's1'}); + const suitesStateById = {s1: {shouldBeShown: true, checkStatus: Number(!checkStatus)}}; + const browsersStateById = {b1: {shouldBeShown: true, checkStatus: Number(!checkStatus)}}; + const tree = mkStateTree({suitesById, suitesStateById, browsersById, browsersStateById}); + const view = mkStateView({keyToGroupTestsBy: ''}); + + const newState = reducer({tree, view}, { + type: actionNames.TOGGLE_SUITE_CHECKBOX, + payload: {suiteId: 's1', checkStatus} + }); + + assert.equal(newState.tree.suites.stateById.s1.checkStatus, checkStatus); + }); + + it(`should not change ${checkStatus ? '' : 'un'}checked state`, () => { + const suitesById = mkSuite({id: 's1', browserIds: ['b1']}); + const browsersById = mkBrowser({id: 'b1', parentId: 's1'}); + const suitesStateById = {s1: {shouldBeShown: true, checkStatus}}; + const browsersStateById = {b1: {shouldBeShown: true, checkStatus}}; + const tree = mkStateTree({suitesById, suitesStateById, browsersById, browsersStateById}); + const view = mkStateView({keyToGroupTestsBy: ''}); + + const newState = reducer({tree, view}, { + type: actionNames.TOGGLE_SUITE_CHECKBOX, + payload: {suiteId: 's1', checkStatus} + }); + + assert.equal(newState.tree.suites.stateById.s1.checkStatus, checkStatus); + assert.equal(newState.tree.browsers.stateById.b1.checkStatus, checkStatus); + }); + + it(`should set child browser checkbox to ${checkStatus ? '' : 'un'}checked state`, () => { + const suitesById = mkSuite({id: 's1', browserIds: ['b1']}); + const browsersById = mkBrowser({id: 'b1', parentId: 's1'}); + const suitesStateById = {s1: {shouldBeShown: true, checkStatus: Number(!checkStatus)}}; + const browsersStateById = {b1: {shouldBeShown: true, checkStatus: Number(!checkStatus)}}; + const tree = mkStateTree({suitesById, suitesStateById, browsersById, browsersStateById}); + const view = mkStateView({keyToGroupTestsBy: ''}); + + const newState = reducer({tree, view}, { + type: actionNames.TOGGLE_SUITE_CHECKBOX, + payload: {suiteId: 's1', checkStatus} + }); + + assert.equal(newState.tree.browsers.stateById.b1.checkStatus, checkStatus); + }); + + it(`should set child suite checkbox to ${checkStatus ? '' : 'un'}checked state`, () => { + const suitesById = { + ...mkSuite({id: 's0', suiteIds: ['s1']}), + ...mkSuite({id: 's1', browserIds: ['b1'], parentId: 's0'}) + }; + const browsersById = mkBrowser({id: 'b1', parentId: 's1'}); + const suitesStateById = { + s0: {shouldBeShown: true, checkStatus: Number(!checkStatus)}, + s1: {shouldBeShown: true, checkStatus: Number(!checkStatus)} + }; + const browsersStateById = {b1: {shouldBeShown: true, checkStatus: Number(!checkStatus)}}; + const tree = mkStateTree({suitesById, suitesStateById, browsersById, browsersStateById}); + const view = mkStateView({keyToGroupTestsBy: ''}); + + const newState = reducer({tree, view}, { + type: actionNames.TOGGLE_SUITE_CHECKBOX, + payload: {suiteId: 's0', checkStatus} + }); + + assert.equal(newState.tree.suites.stateById.s1.checkStatus, checkStatus); + }); + + it(`should update parents to ${checkStatus ? '' : 'un'}checked state`, () => { + const suitesById = { + ...mkSuite({id: 's0', suiteIds: ['s1']}), + ...mkSuite({id: 's1', suiteIds: ['s2'], parentId: 's0'}), + ...mkSuite({id: 's2', browserIds: ['b1'], parentId: 's1'}) + }; + const browsersById = mkBrowser({id: 'b1', parentId: 's2'}); + const suitesStateById = { + s0: {shouldBeShown: true, checkStatus: Number(!checkStatus)}, + s1: {shouldBeShown: true, checkStatus: Number(!checkStatus)}, + s2: {shouldBeShown: true, checkStatus: Number(!checkStatus)} + }; + const browsersStateById = {b1: {shouldBeShown: true, checkStatus: Number(!checkStatus)}}; + const tree = mkStateTree({suitesById, suitesStateById, browsersById, browsersStateById}); + const view = mkStateView({keyToGroupTestsBy: ''}); + + const newState = reducer({tree, view}, { + type: actionNames.TOGGLE_SUITE_CHECKBOX, + payload: {suiteId: 's2', checkStatus} + }); + + assert.equal(newState.tree.suites.stateById.s1.checkStatus, checkStatus); + assert.equal(newState.tree.suites.stateById.s0.checkStatus, checkStatus); + }); + + it(`should update parents to indeterminate from ${checkStatus ? '' : 'un'}checked state`, () => { + const suitesById = { + ...mkSuite({id: 's0', suiteIds: ['s1']}), + ...mkSuite({id: 's1', suiteIds: ['s2', 's3'], parentId: 's0'}), + ...mkSuite({id: 's2', browserIds: ['b1'], parentId: 's1'}), + ...mkSuite({id: 's3', browserIds: ['b2'], parentId: 's1'}) + }; + const browsersById = { + ...mkBrowser({id: 'b1', parentId: 's2'}), + ...mkBrowser({id: 'b2', parentId: 's3'}) + }; + const suitesStateById = { + s0: {shouldBeShown: true, checkStatus}, + s1: {shouldBeShown: true, checkStatus}, + s2: {shouldBeShown: true, checkStatus}, + s3: {shouldBeShown: true, checkStatus} + }; + const browsersStateById = { + b1: {shouldBeShown: true, checkStatus}, + b2: {shouldBeShown: true, checkStatus} + }; + const tree = mkStateTree({suitesById, suitesStateById, browsersById, browsersStateById}); + const view = mkStateView({keyToGroupTestsBy: ''}); + + const newState = reducer({tree, view}, { + type: actionNames.TOGGLE_SUITE_CHECKBOX, + payload: {suiteId: 's2', checkStatus: Number(!checkStatus)} + }); + + assert.equal(newState.tree.suites.stateById.s1.checkStatus, INDETERMINATE); + assert.equal(newState.tree.suites.stateById.s0.checkStatus, INDETERMINATE); + }); + }); + }); + + describe(`${actionNames.TOGGLE_GROUP_CHECKBOX} action`, () => { + const mkState = ({checkStatus}) => { + const suitesById = mkSuite({id: 's1', browserIds: ['b1']}); + const browsersById = mkBrowser({id: 'b1', parentId: 's1'}); + const suitesStateById = {s1: {shouldBeShown: true, checkStatus}}; + const browsersStateById = {b1: {shouldBeShown: true, checkStatus}}; + const tree = mkStateTree({suitesById, suitesStateById, browsersById, browsersStateById}); + const view = mkStateView({keyToGroupTestsBy: ''}); + + return {tree, view}; + }; + + [CHECKED, UNCHECKED].forEach(checkStatus => { + it(`should not change ${checkStatus ? '' : 'un'}checked state`, () => { + const state = mkState({checkStatus}); + + const newState = reducer(state, { + type: actionNames.TOGGLE_GROUP_CHECKBOX, + payload: {browserIds: ['b1'], checkStatus} + }); + + assert.equal(newState.tree.browsers.stateById.b1.checkStatus, checkStatus); + }); + + it(`should set browsers checkbox to ${checkStatus ? '' : 'un'}checked state`, () => { + const state = mkState({checkStatus: Number(!checkStatus)}); + + const newState = reducer(state, { + type: actionNames.TOGGLE_GROUP_CHECKBOX, + payload: {browserIds: ['b1'], checkStatus} + }); + + assert.equal(newState.tree.browsers.stateById.b1.checkStatus, checkStatus); + }); + + it(`should set child suite checkbox to ${checkStatus ? '' : 'un'}checked state`, () => { + const state = mkState({checkStatus: Number(!checkStatus)}); + + const newState = reducer(state, { + type: actionNames.TOGGLE_GROUP_CHECKBOX, + payload: {browserIds: ['b1'], checkStatus} + }); + + assert.equal(newState.tree.suites.stateById.s1.checkStatus, checkStatus); + }); + }); + }); + + [ + actionNames.BROWSERS_SELECTED, + actionNames.CHANGE_VIEW_MODE, + actionNames.VIEW_UPDATE_FILTER_BY_NAME, + actionNames.VIEW_SET_STRICT_MATCH_FILTER + ].forEach(actionName => { + describe(`${actionName} action`, () => { + const mkActionStateView = ({viewMode, testNameFilter, strictMatchFilter, filteredBrowsers}) => { + const actionToMkView = { + [actionNames.CHANGE_VIEW_MODE]: () => mkStateView({viewMode}), + [actionNames.VIEW_UPDATE_FILTER_BY_NAME]: () => mkStateView({testNameFilter}), + [actionNames.VIEW_SET_STRICT_MATCH_FILTER]: () => mkStateView({testNameFilter, strictMatchFilter}), + [actionNames.BROWSERS_SELECTED]: () => mkStateView({filteredBrowsers}) + }; + + return actionToMkView[actionName](); + }; + + it('should uncheck unshown tests', () => { + const browsersById = { + ...mkBrowser({id: 'b1', name: 'b1', resultIds: ['r1'], parentId: 's1'}), + ...mkBrowser({id: 'b2', name: 'b2', resultIds: ['r2'], parentId: 's1'}) + }; + const resultsById = { + ...mkResult({id: 'r1', status: SUCCESS}), + ...mkResult({id: 'r2', status: SUCCESS}) + }; + const browsersStateById = { + b1: {checkStatus: CHECKED, shouldBeShown: true}, + d1: {checkStatus: CHECKED, shouldBeShown: true} + }; + const suitesById = mkSuite({id: 's1', browserIds: ['b1', 'b2'], parentId: null, root: true}); + const suitesStateById = {'s1': {checkStatus: CHECKED, shouldBeShown: true, shouldBeOpened: true}}; + const suitesAllRootIds = ['s1']; + const tree = mkStateTree({suitesById, browsersById, resultsById, suitesStateById, browsersStateById, suitesAllRootIds}); + const view = mkActionStateView({ + viewMode: viewModes.FAILED, + testNameFilter: 's2', + strictMatchFilter: true, + filteredBrowsers: [{id: 'c1', versions: []}] + }); + + const newState = reducer({tree, view}, {type: actionName}); + + assert.equal(newState.tree.browsers.stateById.b1.checkStatus, UNCHECKED); + assert.equal(newState.tree.browsers.stateById.b2.checkStatus, UNCHECKED); + }); + + it('should save check on shown tests', () => { + const browsersById = mkBrowser({id: 'b1', name: 'b1', resultIds: ['r1'], parentId: 's1'}); + const resultsById = mkResult({id: 'r1', status: FAIL}); + const browsersStateById = {b1: {checkStatus: CHECKED, shouldBeShown: true}}; + const suitesById = mkSuite({id: 's1', browserIds: ['b1'], parentId: null, root: true}); + const suitesStateById = {s1: {checkStatus: CHECKED, shouldBeShown: true, shouldBeOpened: true}}; + const suitesAllRootIds = ['s1']; + const tree = mkStateTree({suitesById, browsersById, resultsById, suitesStateById, browsersStateById, suitesAllRootIds}); + const view = mkActionStateView({ + viewMode: viewModes.FAILED, + testNameFilter: 's1', + strictMatchFilter: true, + filteredBrowsers: [{id: 'b1', versions: []}] + }); + + const newState = reducer({tree, view}, {type: actionName}); + + assert.equal(newState.tree.browsers.stateById.b1.checkStatus, CHECKED); + }); + + it('should update parent suite', () => { + const browsersById = { + ...mkBrowser({id: 'b1', name: 'b1', resultIds: ['r1'], parentId: 's1'}), + ...mkBrowser({id: 'd1', name: 'd1', resultIds: ['r2'], parentId: 's2'}) + }; + const resultsById = { + ...mkResult({id: 'r1', status: FAIL}), + ...mkResult({id: 'r2', status: SUCCESS}) + }; + const browsersStateById = { + b1: {checkStatus: CHECKED, shouldBeShown: true}, + d1: {checkStatus: UNCHECKED, shouldBeShown: true} + }; + const suitesById = { + ...mkSuite({id: 's0', suiteIds: ['s1', 's2'], parentId: null, root: true}), + ...mkSuite({id: 's1', browserIds: ['b1'], parentId: 's0'}), + ...mkSuite({id: 's2', browserIds: ['d1'], parentId: 's0'}) + }; + const suitesStateById = { + s0: {checkStatus: INDETERMINATE, shouldBeShown: true, shouldBeOpened: true}, + s1: {checkStatus: CHECKED, shouldBeShown: true, shouldBeOpened: true}, + s2: {checkStatus: UNCHECKED, shouldBeShown: false, shouldBeOpened: false} + }; + const suitesAllRootIds = ['s1']; + const tree = mkStateTree({suitesById, browsersById, resultsById, suitesStateById, browsersStateById, suitesAllRootIds}); + const view = mkActionStateView({ + viewMode: viewModes.FAILED, + testNameFilter: 's1', + strictMatchFilter: true, + filteredBrowsers: [{id: 'b1', versions: []}] + }); + + const newState = reducer({tree, view}, {type: actionName}); + + assert.equal(newState.tree.suites.stateById.s0.checkStatus, CHECKED); + assert.equal(newState.tree.suites.stateById.s1.checkStatus, CHECKED); + }); + }); + }); });