From 027a077d63c5ab485b8ae0b173f99ebbdc90ecfe Mon Sep 17 00:00:00 2001 From: Adam Gruber Date: Fri, 13 Dec 2019 13:14:31 -0500 Subject: [PATCH] Feature/quick summary filters (#138) * Add `toggleSingleFilter` method Handles quick summary filtering * Update quick summary icons to be filter buttons Add styles for filter states * Wire up quick summary filtering * Make filters a property of the store * Add intialFilterState to test * update changelog * Add quick summary component tests to fix coverage --- CHANGELOG.md | 2 + src/client/components/navbar/index.js | 9 +- src/client/components/quick-summary/index.js | 35 +++++--- .../quick-summary/quick-summary.css | 57 +++++++++++- src/client/components/report.js | 4 +- src/client/js/reportStore.js | 44 ++++++++++ test/spec/components/quick-summary.test.js | 23 +++++ test/spec/reportStore.test.js | 87 ++++++++++++++----- 8 files changed, 226 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8cc5936..057f7fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # mochawesome-report-generator changelog ## [Unreleased] +### Added +- Clicking icons in navbar enable quick filtering of single test type ## [4.0.1] / 2019-07-05 ### Changed diff --git a/src/client/components/navbar/index.js b/src/client/components/navbar/index.js index 12e261b3..26f9971d 100644 --- a/src/client/components/navbar/index.js +++ b/src/client/components/navbar/index.js @@ -6,7 +6,7 @@ import styles from './navbar.css'; const cx = classNames.bind(styles); -const Navbar = ({ onMenuClick, reportTitle, stats }) => { +const Navbar = ({ onMenuClick, onQuickFilterClick, reportTitle, singleFilter, stats }) => { const { passPercent, pendingPercent } = stats; const failPercent = 100 - passPercent; @@ -35,7 +35,10 @@ const Navbar = ({ onMenuClick, reportTitle, stats }) => {
- +
{showPctBar && (
@@ -50,7 +53,9 @@ const Navbar = ({ onMenuClick, reportTitle, stats }) => { Navbar.propTypes = { onMenuClick: PropTypes.func, + onQuickFilterClick: PropTypes.func, reportTitle: PropTypes.string, + singleFilter: PropTypes.string, stats: PropTypes.object, }; diff --git a/src/client/components/quick-summary/index.js b/src/client/components/quick-summary/index.js index 15a22673..458f9cfe 100644 --- a/src/client/components/quick-summary/index.js +++ b/src/client/components/quick-summary/index.js @@ -6,7 +6,7 @@ import styles from './quick-summary.css'; const cx = classNames.bind(styles); -const QuickSummary = ({ stats }) => { +const QuickSummary = ({ onQuickFilterClick, singleFilter, stats }) => { const { duration, suites, @@ -16,6 +16,11 @@ const QuickSummary = ({ stats }) => { pending, skipped, } = stats; + + const filterClasses = singleFilter + ? ['single-filter', `single-filter--${singleFilter.slice(4).toLowerCase()}`] + : ''; + return (
    @@ -36,25 +41,33 @@ const QuickSummary = ({ stats }) => { {testsRegistered}
-
    +
    • - - {passes} +
    • - - {failures} +
    • {!!pending && (
    • - - {pending} +
    • )} {!!skipped && (
    • - - {skipped} +
    • )}
    @@ -63,6 +76,8 @@ const QuickSummary = ({ stats }) => { }; QuickSummary.propTypes = { + onQuickFilterClick: PropTypes.func, + singleFilter: PropTypes.string, stats: PropTypes.object, }; diff --git a/src/client/components/quick-summary/quick-summary.css b/src/client/components/quick-summary/quick-summary.css index 2821cae4..610a2ca0 100644 --- a/src/client/components/quick-summary/quick-summary.css +++ b/src/client/components/quick-summary/quick-summary.css @@ -23,6 +23,19 @@ font-size: 16px; flex-basis: 25%; + & button { + @extend %button-base; + + display: flex; + align-items: center; + color: #fff; + cursor: pointer; + + &:hover .icon { + border-color: #fff; + } + } + &.tests { color: #fff; } @@ -31,6 +44,16 @@ & .icon { color: var(--green700); background-color: var(--green100); + + @nest .single-filter & { + background-color: var(--grey300); + color: var(--grey500); + } + + @nest .single-filter--passed & { + color: #fff; + background-color: var(--green700); + } } } @@ -38,6 +61,16 @@ & .icon { color: var(--red700); background-color: var(--red100); + + @nest .single-filter & { + background-color: var(--grey300); + color: var(--grey500); + } + + @nest .single-filter--failed & { + color: #fff; + background-color: var(--red700); + } } } @@ -45,6 +78,16 @@ & .icon { color: var(--ltblue700); background-color: var(--ltblue100); + + @nest .single-filter & { + background-color: var(--grey300); + color: var(--grey500); + } + + @nest .single-filter--pending & { + color: #fff; + background-color: var(--ltblue700); + } } } @@ -52,6 +95,16 @@ & .icon { color: var(--grey700); background-color: var(--grey100); + + @nest .single-filter & { + background-color: var(--grey300); + color: var(--grey500); + } + + @nest .single-filter--skipped & { + color: #fff; + background-color: var(--grey700); + } } } } @@ -67,7 +120,9 @@ .circle-icon { font-size: 12px; border-radius: 50%; - padding: 3px; + padding: 2px; + border: 1px solid transparent; + transition: border-color 0.2s ease-out; } /* Tablet 768 and up */ diff --git a/src/client/components/report.js b/src/client/components/report.js index 79aa9a9f..2d35c564 100644 --- a/src/client/components/report.js +++ b/src/client/components/report.js @@ -7,13 +7,15 @@ import 'styles/app.global.css'; import MobxDevTool from './mobxDevtool'; const MochawesomeReport = observer(props => { - const { openSideNav, reportTitle, stats, devMode, VERSION } = props.store; + const { openSideNav, toggleSingleFilter, singleFilter, reportTitle, stats, devMode, VERSION } = props.store; return (
    diff --git a/src/client/js/reportStore.js b/src/client/js/reportStore.js index ad468ef4..2d5ce912 100644 --- a/src/client/js/reportStore.js +++ b/src/client/js/reportStore.js @@ -12,7 +12,9 @@ class ReportStore { devMode: !!config.dev, enableChart: !!config.enableCharts, enableCode: !!config.enableCode, + filters: ['showPassed', 'showFailed', 'showPending', 'showSkipped'], initialLoadTimeout: 300, + initialFilterState: null, reportTitle: config.reportTitle || data.reportTitle, results: data.results || [], showHooksOptions: ['failed', 'always', 'never', 'context'], @@ -29,10 +31,21 @@ class ReportStore { showPending: config.showPending !== undefined ? config.showPending : true, showSkipped: config.showSkipped !== undefined ? config.showSkipped : false, + singleFilter: null, sideNavOpen: false, }, { filteredSuites: observable.shallow }); + + this.initialize(); + } + + initialize() { + // Save initial filter state so we can restore after quick filtering + this.initialFilterState = this.filters.reduce((acc, filter) => ({ + ...acc, + [filter]: this[filter], + }), {}) } @action.bound @@ -48,9 +61,34 @@ class ReportStore { @action.bound toggleFilter(prop) { this.toggleIsLoading(true); + // Clear single filter prop + this.singleFilter = null; this[prop] = !this[prop]; } + @action.bound + toggleSingleFilter(prop) { + // Not in single filter mode or changing single filter + if (this.singleFilter !== prop) { + // Set filters to false + this.filters.filter(filter => filter !== prop).forEach(filter => { + this[filter] = false; + }); + + // Set single filter to true + this[prop] = true; + + // Update single filter prop + this.singleFilter = prop; + } else { + // Restore filters + this._restoreInitialFilterState() + + // Clear single filter prop + this.singleFilter = null; + } + } + @action.bound setShowHooks(prop) { if (this._isValidShowHookOption(prop)) { @@ -140,6 +178,12 @@ class ReportStore { : showHooksDefault; }; + _restoreInitialFilterState = () => { + this.filters.forEach(filter => { + this[filter] = this.initialFilterState[filter]; + }); + }; + updateFilteredSuites(timeout = this.initialLoadTimeout) { setTimeout(() => { this.toggleIsLoading(false); diff --git a/test/spec/components/quick-summary.test.js b/test/spec/components/quick-summary.test.js index 1ec18eff..2bdc3459 100644 --- a/test/spec/components/quick-summary.test.js +++ b/test/spec/components/quick-summary.test.js @@ -2,6 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import chai, { expect } from 'chai'; import chaiEnzyme from 'chai-enzyme'; +import sinon from 'sinon'; import QuickSummary from 'components/quick-summary'; @@ -15,11 +16,14 @@ describe('', () => { return { wrapper, lists: wrapper.find('.quick-summary-list'), + filterBtns: wrapper.find('button'), }; }; beforeEach(() => { props = { + onQuickFilterClick: sinon.spy(), + singleFilter: null, stats: { duration: 532, suites: 14, @@ -47,4 +51,23 @@ describe('', () => { expect(lists.at(0).find('.quick-summary-item')).to.have.lengthOf(3); expect(lists.at(1).find('.quick-summary-item')).to.have.lengthOf(2); }); + + it('renders with `singleFilter` set', () => { + props.singleFilter = 'showPassed'; + const { lists } = getInstance(props); + expect(lists.at(1)).to.have.className('single-filter'); + expect(lists.at(1)).to.have.className('single-filter--passed'); + }); + + describe('quick filters', () => { + it('should call `onQuickFilterClick` with expected argument', () => { + const { filterBtns } = getInstance(props); + const filters = ['showPassed', 'showFailed', 'showPending', 'showSkipped']; + filterBtns.forEach((btn, i) => { + btn.simulate('click'); + expect(props.onQuickFilterClick.getCall(i).args[0]).to.equal(filters[i]); + }) + }); + }); + }); diff --git a/test/spec/reportStore.test.js b/test/spec/reportStore.test.js index 86e8fa66..e5681bf8 100644 --- a/test/spec/reportStore.test.js +++ b/test/spec/reportStore.test.js @@ -18,12 +18,24 @@ describe('ReportStore', () => { it('has the correct default state', () => { store = createStore(); expect(store).to.have.deep.property('results', []); + expect(store).to.have.deep.property('filters', [ + 'showPassed', + 'showFailed', + 'showPending', + 'showSkipped', + ]); expect(store).to.have.deep.property('showHooksOptions', [ 'failed', 'always', 'never', 'context', ]); + expect(store).to.have.deep.property('initialFilterState', { + showPassed: true, + showFailed: true, + showPending: true, + showSkipped: false, + }); expect(store).to.include({ devMode: false, enableChart: false, @@ -93,28 +105,61 @@ describe('ReportStore', () => { expect(store).to.have.property('sideNavOpen', false); }); - it('toggleFilter, showPassed', () => { - expect(store).to.have.property('showPassed', true); - store.toggleFilter('showPassed'); - expect(store).to.have.property('showPassed', false); - }); - - it('toggleFilter, showFailed', () => { - expect(store).to.have.property('showFailed', true); - store.toggleFilter('showFailed'); - expect(store).to.have.property('showFailed', false); - }); - - it('toggleFilter, showPending', () => { - expect(store).to.have.property('showPending', true); - store.toggleFilter('showPending'); - expect(store).to.have.property('showPending', false); - }); + describe('toggleFilter', () => { + [ + ['showPassed', true], + ['showFailed', true], + ['showPending', true], + ['showSkipped', false] + ].forEach(([filter, initial]) => { + it(`${filter}`, () => { + expect(store).to.have.property(filter, initial); + store.toggleFilter(filter); + expect(store).to.have.property(filter, !initial); + }); + }) + }); + + describe('toggleSingleFilter', () => { + const filters = ['showPassed', 'showFailed', 'showPending', 'showSkipped']; + describe('when `singleFilter` is NOT set', () => { + filters.forEach(filter => { + it(`should set expected filter state when toggling: ${filter}`, () => { + store.toggleSingleFilter(filter); + filters.forEach(f => { + if (f === filter) { + expect(store[f]).to.equal(true); + } else { + expect(store[f]).to.equal(false); + } + }) + }); + }) + }); - it('toggleFilter, showSkipped', () => { - expect(store).to.have.property('showSkipped', false); - store.toggleFilter('showSkipped'); - expect(store).to.have.property('showSkipped', true); + describe('when `singleFilter` is set', () => { + beforeEach(() => { + store.singleFilter = 'showPassed'; + }); + + it('should set expected filter state when toggling active filter', () => { + store.toggleSingleFilter('showPassed'); + filters.forEach(f => { + expect(store[f]).to.equal(store.initialFilterState[f]); + }); + }); + + it('should set expected filter state when toggling different filter', () => { + store.toggleSingleFilter('showFailed'); + filters.forEach(f => { + if (f === 'showFailed') { + expect(store[f]).to.equal(true); + } else { + expect(store[f]).to.equal(false); + } + }); + }); + }); }); it('setShowHooks', () => {