diff --git a/.eslintrc.json b/.eslintrc.json index ec24ab7..58eddc6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,11 +1,16 @@ { "extends": [ "eslint:recommended", + "plugin:jsx-a11y/recommended", "plugin:react/recommended", "plugin:prettier/recommended", + "plugin:sonarjs/recommended", + "plugin:security/recommended", + "plugin:react-hooks/recommended", + "xo", + "xo-react", "prettier", - "prettier/react", - "plugin:react-hooks/recommended" + "prettier/react" ], "parser": "babel-eslint", "rules": { @@ -41,13 +46,25 @@ "no-multiple-empty-lines": "error", "prefer-const": "error", "no-use-before-define": "error", - "prettier/prettier": "error" + "prettier/prettier": "error", + "comma-dangle": "off", + "indent": "off", + "object-curly-spacing": "off", + "no-tabs": ["error", { "allowIndentationTabs": true }], + "operator-linebreak": "off", + "no-else-return": "off", + "react/no-array-index-key": "off", + "jsx-a11y/click-events-have-key-events": "off", + "sonarjs/no-duplicate-string": "off", + "jsx-a11y/no-static-element-interactions": "off" }, "plugins": ["prettier"], "parserOptions": { - "sourceType": "module", - "allowImportExportEverywhere": false, - "codeFrame": true + "ecmaVersion": 2020, // Allows for the parsing of modern ECMAScript features + "sourceType": "module", // Allows for the use of imports + "ecmaFeatures": { + "jsx": true // Allows for the parsing of JSX + } }, "env": { "browser": true, diff --git a/package.json b/package.json index ce85a86..6af49e8 100644 --- a/package.json +++ b/package.json @@ -73,12 +73,17 @@ "clean-webpack-plugin": "^3.0.0", "css-loader": "^5.0.1", "eslint": "^7.4.0", - "eslint-config-prettier": "^7.1.0", - "eslint-loader": "^4.0.2", + "eslint-config-prettier": "^7.2.0", + "eslint-config-xo": "^0.35.0", + "eslint-config-xo-react": "^0.23.0", + "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^4.0.8", - "html-loader": "^1.1.0", + "eslint-plugin-security": "^1.4.0", + "eslint-plugin-sonarjs": "^0.6.0", + "eslint-webpack-plugin": "^2.4.0", + "html-loader": "^2.0.0", "html-webpack-plugin": "4", "identity-obj-proxy": "^3.0.0", "jest": "^26.1.0", @@ -91,8 +96,8 @@ "ts-loader": "^8.0.4", "typescript": "^4.1.2", "webpack": "^5.6.0", - "webpack-cli": "^4.5.0", - "webpack-dev-server": "^3.11.0", + "webpack-cli": "^4.2.0", + "webpack-dev-server": "^3.11.2", "webpack-merge": "^5.0.9" } } diff --git a/src/App.js b/src/App.js index c689580..7276c66 100644 --- a/src/App.js +++ b/src/App.js @@ -11,6 +11,7 @@ class App extends Component { constructor(props) { data = window.resultData; super(props); + // eslint-disable-next-line react/state-in-constructor this.state = { menuState: 'close', testResults: data, @@ -21,8 +22,10 @@ class App extends Component { this.state.gridData = this.state.treeViewData; this.menuStateChange = this.menuStateChange.bind(this); this.onStatusChecked = this.onStatusChecked.bind(this); - this.state.toggleState = this.state.testResults?.reporterOptions?.expandResults; + this.state.isResultExpanded = this.state.testResults?.reporterOptions?.expandResults; + this.onExpandToggle = this.onExpandToggle.bind(this); } + getStatusList() { return statusList; } @@ -50,10 +53,12 @@ class App extends Component { statusFilter, ); } + treeViewData = rootElement; return treeViewData; } + // eslint-disable-next-line sonarjs/cognitive-complexity parseTreeData(testResults, parentArray, id, statusFilter) { let subArray = []; testResults.forEach(element => { @@ -90,6 +95,7 @@ class App extends Component { if (statusList.indexOf(element.status) < 0) { statusList.push(element.status); } + if ( statusFilter.length === 0 || statusFilter.indexOf(element.status) >= 0 @@ -108,9 +114,11 @@ class App extends Component { if (subArray.length > 0) { parentArray = subArray; } + return [parentArray, id]; } + // eslint-disable-next-line max-params, sonarjs/cognitive-complexity parseAncestor(ancestors, testCase, parentArray, id, statusFilter) { const ancestorCopy = [...ancestors]; if (ancestors.length > 0) { @@ -120,15 +128,16 @@ class App extends Component { }); if (elementIndex === -1) { - const nodeValue = {}; - nodeValue.title = itemTitle; - nodeValue.numPassedTests = testCase.status === 'passed' ? 1 : 0; - nodeValue.numFailedTests = testCase.status === 'failed' ? 1 : 0; - nodeValue.numTotalTests = 1; - nodeValue.numPendingTests = - testCase.status === 'pending' ? 1 : 0; - nodeValue.numTodoTests = testCase.status === 'todo' ? 1 : 0; - nodeValue.id = `id${id}`; + const nodeValue = { + title: itemTitle, + numPassedTests: testCase.status === 'passed' ? 1 : 0, + numFailedTests: testCase.status === 'failed' ? 1 : 0, + numTotalTests: 1, + numPendingTests: testCase.status === 'pending' ? 1 : 0, + numTodoTests: testCase.status === 'todo' ? 1 : 0, + id: `id${id}`, + }; + id++; ancestorCopy.shift(); [nodeValue.children, id] = this.parseAncestor( @@ -141,26 +150,34 @@ class App extends Component { parentArray.push(nodeValue); } else { ancestorCopy.shift(); + // eslint-disable-next-line security/detect-object-injection parentArray[elementIndex].numTotalTests++; switch (testCase.status) { case 'passed': + // eslint-disable-next-line security/detect-object-injection parentArray[elementIndex].numPassedTests++; break; case 'failed': + // eslint-disable-next-line security/detect-object-injection parentArray[elementIndex].numFailedTests++; break; case 'pending': + // eslint-disable-next-line security/detect-object-injection parentArray[elementIndex].numPendingTests++; break; case 'todo': + // eslint-disable-next-line security/detect-object-injection parentArray[elementIndex].numTodoTests++; break; default: break; } + + // eslint-disable-next-line security/detect-object-injection [parentArray[elementIndex].children, id] = this.parseAncestor( ancestorCopy, testCase, + // eslint-disable-next-line security/detect-object-injection parentArray[elementIndex].children, id, statusFilter, @@ -173,6 +190,7 @@ class App extends Component { id++; parentArray.push(nodeValue); } + return [parentArray, id]; } @@ -203,10 +221,11 @@ class App extends Component { }); information.push({ title: 'Elapsed', - value: data?.endTime - data?.startTime, + value: data.endTime - data.startTime, type: 'time', }); } + if (data?.openHandles && data.openHandles.length > 0) { information.push({ title: 'Open Handles', @@ -214,6 +233,7 @@ class App extends Component { type: 'number', }); } + information.push({ title: 'Interupted', value: data?.wasInterrupted, @@ -223,37 +243,39 @@ class App extends Component { } onStatusChecked = checkedStatuses => { - this.setState({ + this.setState(prevState => ({ gridData: this.formatTreeViewData( - this.state.testResults, + prevState.testResults, checkedStatuses, ), - }); + })); }; - onExpandToggle = toggleState => { - this.setState({ toggleState: toggleState }); + onExpandToggle = isResultExpanded => { + this.setState({ isResultExpanded: isResultExpanded }); }; render() { return (
{ test('Should contain minimal information', () => { window.resultData = data; - const { container } = render(); + const { container } = render(); expect( container.querySelector('.infoWrapper').childNodes.length, ).toEqual(4); @@ -18,7 +18,7 @@ describe('Page Information', () => { }; infoData.reporterOptions = reporterOptions; window.resultData = infoData; - const { container } = render(); + const { container } = render(); expect( container.querySelector('.infoWrapper').childNodes.length, ).toEqual(6); @@ -26,7 +26,7 @@ describe('Page Information', () => { test('Should contain project level information', () => { const infoData = { ...sampleData }; window.resultData = infoData; - const { container } = render(); + const { container } = render(); expect( container.querySelector('.infoWrapper').childNodes.length, ).toEqual(8); @@ -35,7 +35,7 @@ describe('Page Information', () => { const infoData = { ...sampleData }; infoData.openHandles = ['handle']; window.resultData = infoData; - const { container } = render(); + const { container } = render(); expect( container.querySelector('.infoWrapper').childNodes.length, ).toEqual(10); @@ -45,7 +45,7 @@ describe('Page Information', () => { describe('Menu click', () => { test('Should show and hide tree on click', () => { window.resultData = data; - const { container } = render(); + const { container } = render(); fireEvent.click(container.querySelector('#menu')); expect( container.querySelector('.sidenav').classList.contains('open'), @@ -60,7 +60,7 @@ describe('Menu click', () => { describe('Tree click', () => { test('Should call function on tree node click', () => { window.resultData = data; - const { container } = render(); + const { container } = render(); const date = container.querySelector('.box2'); date.textContent = ''; fireEvent.click(container.querySelector('#menu')); @@ -79,12 +79,12 @@ describe('Tree click', () => { describe('Status filter', () => { test('Should have checkboxes', () => { window.resultData = sampleData; - const { container } = render(); + const { container } = render(); expect(container.querySelectorAll('.checkboxLabel').length).toEqual(4); }); test('Should filter on checkbox click', () => { window.resultData = sampleData; - const { container } = render(); + const { container } = render(); const statusCheckboxes = container.querySelectorAll('.checkboxLabel'); fireEvent.click(statusCheckboxes[3]); expect(container.querySelectorAll('.tab-content').length).toEqual(2); @@ -95,7 +95,7 @@ describe('Status filter', () => { describe('Toggle Button', () => { test('Should fire event', () => { window.resultData = data; - const { container } = render(); + const { container } = render(); expect( container.querySelectorAll('.togglerCheckBox:checked'), ).toHaveLength(0); @@ -104,4 +104,15 @@ describe('Toggle Button', () => { container.querySelectorAll('.togglerCheckBox:checked'), ).toHaveLength(12); }); + + test('Should use default expand results flag', () => { + const copiedData = JSON.parse(JSON.stringify(data)); + copiedData.reporterOptions = {}; + copiedData.reporterOptions.expandResults = true; + window.resultData = copiedData; + const { container } = render(); + expect( + container.querySelectorAll('.togglerCheckBox:checked'), + ).toHaveLength(12); + }); }); diff --git a/src/Components/FilterToggler/CheckBox.js b/src/Components/FilterToggler/CheckBox.js index 4bfe208..6c897b6 100644 --- a/src/Components/FilterToggler/CheckBox.js +++ b/src/Components/FilterToggler/CheckBox.js @@ -7,16 +7,17 @@ export const CheckBox = props => { {props.value} - + ); }; + CheckBox.propTypes = { handleCheck: PropTypes.func.isRequired, isChecked: PropTypes.bool, diff --git a/src/Components/FilterToggler/CheckBox.test.js b/src/Components/FilterToggler/CheckBox.test.js index 8182f15..fa1b0e5 100644 --- a/src/Components/FilterToggler/CheckBox.test.js +++ b/src/Components/FilterToggler/CheckBox.test.js @@ -3,33 +3,21 @@ import CheckBox from './CheckBox'; import { render, fireEvent } from '@testing-library/react'; test('Should contain value', () => { const { container } = render( - , + {}} />, ); expect(container).toHaveTextContent('Test'); }); test('Should not be checked', () => { const { container } = render( - , + {}} />, ); expect(container.firstChild.lastChild.previousSibling).not.toBeChecked(); }); test('Should be checked', () => { const { container } = render( - , + {}} />, ); expect(container.firstChild.lastChild.previousSibling).toBeChecked(); }); @@ -37,11 +25,7 @@ test('Should be checked', () => { test('Should call function on change', () => { const mockCallback = jest.fn(); const { container } = render( - , + , ); fireEvent.click(container.firstChild); expect(mockCallback.mock.calls.length).toBe(1); diff --git a/src/Components/FilterToggler/FilterToggler.js b/src/Components/FilterToggler/FilterToggler.js index 72881bb..377fa8f 100644 --- a/src/Components/FilterToggler/FilterToggler.js +++ b/src/Components/FilterToggler/FilterToggler.js @@ -5,6 +5,7 @@ import './FilterToggler.css'; export default class FilterToggler extends React.Component { constructor(props) { super(props); + // eslint-disable-next-line react/state-in-constructor this.state = { statusList: this.init(this.props.statusList), }; @@ -26,13 +27,15 @@ export default class FilterToggler extends React.Component { if (status.value === event.target.value) { status.isChecked = event.target.checked; } + if (status.isChecked) { checkStatuses.push(status.value); } }); - this.setState(this.state.statusList); + this.setState(prevState => ({ statusList: prevState.statusList })); this.props.onStatusChecked(checkStatuses); }; + render() { if (this.props.statusList && this.props.statusList.length > 0) { return ( @@ -51,12 +54,17 @@ export default class FilterToggler extends React.Component {

); - } else { - return null; } + + return null; } + + static propTypes = { + statusList: PropTypes.array, + onStatusChecked: PropTypes.func.isRequired, + }; + + static defaultProps = { + statusList: [], + }; } -FilterToggler.propTypes = { - statusList: PropTypes.array, - onStatusChecked: PropTypes.func.isRequired, -}; diff --git a/src/Components/FilterToggler/FilterToggler.test.js b/src/Components/FilterToggler/FilterToggler.test.js index 9090d48..cc387ea 100644 --- a/src/Components/FilterToggler/FilterToggler.test.js +++ b/src/Components/FilterToggler/FilterToggler.test.js @@ -3,10 +3,7 @@ import FilterToggler from './FilterToggler'; import { render, fireEvent } from '@testing-library/react'; test('Should contain one checkbox', () => { const { container } = render( - , + {}} />, ); expect(container.getElementsByTagName('input').length).toEqual(1); }); @@ -15,8 +12,8 @@ test('Should contain two checkbox', () => { const { container } = render( , + onStatusChecked={() => {}} + />, ); expect(container.getElementsByTagName('input').length).toEqual(2); }); @@ -27,7 +24,7 @@ test('Should call function on change', () => { , + />, ); fireEvent.click(container.firstChild.getElementsByTagName('input')[0]); expect(mockCallback.mock.calls.length).toBe(1); diff --git a/src/Components/Grid/GridTabView.js b/src/Components/Grid/GridTabView.js index cd0e8be..160a613 100644 --- a/src/Components/Grid/GridTabView.js +++ b/src/Components/Grid/GridTabView.js @@ -5,24 +5,29 @@ import PropTypes from 'prop-types'; class GridTabView extends Component { render() { return ( - + <> {this.props.testResults.map(item => { return ( ); })} - + ); } + + static propTypes = { + testResults: PropTypes.any.isRequired, + onShowModel: PropTypes.func.isRequired, + isResultExpanded: PropTypes.any, + }; + + static defaultProps = { + isResultExpanded: null, + }; } -GridTabView.propTypes = { - testResults: PropTypes.any.isRequired, - onShowModel: PropTypes.func.isRequired, - expandResults: PropTypes.any, -}; export default GridTabView; diff --git a/src/Components/Grid/Status.js b/src/Components/Grid/Status.js index 5ebe13f..93aef1f 100644 --- a/src/Components/Grid/Status.js +++ b/src/Components/Grid/Status.js @@ -5,7 +5,7 @@ class Status extends Component { render() { if (this.props.status === 'passed') { return ( - + <> + /> {this.props.status} - + ); } else if (this.props.status === 'failed') { return ( - + <> + /> {this.props.status} - + ); } else if (this.props.status === 'pending') { return ( - + <> + /> {this.props.status} - + ); } else if (this.props.status === 'todo') { return ( - + <> + /> {this.props.status} - + ); } else { - return {this.props.status}; + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{this.props.status}; } } + + static propTypes = { + status: PropTypes.string, + }; + + static defaultProps = { + status: '', + }; } -Status.propTypes = { - status: PropTypes.string, -}; export default Status; diff --git a/src/Components/Grid/Status.test.js b/src/Components/Grid/Status.test.js index dcedbc8..67780e9 100644 --- a/src/Components/Grid/Status.test.js +++ b/src/Components/Grid/Status.test.js @@ -2,22 +2,22 @@ import React from 'react'; import { render } from '@testing-library/react'; import Status from './Status'; test('Should contain passed', () => { - const { container } = render(); + const { container } = render(); expect(container.textContent).toEqual('passed'); expect(container).toMatchSnapshot(); }); test('Should contain failed', () => { - const { container } = render(); + const { container } = render(); expect(container.textContent).toEqual('failed'); expect(container).toMatchSnapshot(); }); test('Should contain pending', () => { - const { container } = render(); + const { container } = render(); expect(container.textContent).toEqual('pending'); expect(container).toMatchSnapshot(); }); test('Should contain todo', () => { - const { container } = render(); + const { container } = render(); expect(container.textContent).toEqual('todo'); expect(container).toMatchSnapshot(); }); diff --git a/src/Components/Grid/TabContent.js b/src/Components/Grid/TabContent.js index 3cbfbed..7df4e92 100644 --- a/src/Components/Grid/TabContent.js +++ b/src/Components/Grid/TabContent.js @@ -9,6 +9,7 @@ class TabContent extends Component { formatTime(value) { return new DateUtilities().convertMillisecondsToTime(value); } + render() { const hasChildren = this.props.item.children && this.props.item.children.length > 0; @@ -16,15 +17,15 @@ class TabContent extends Component { if (hasChildren) { content = ( ); } else { content = ( - + <>
{this.props.item.title}
@@ -51,21 +52,28 @@ class TabContent extends Component { fill="currentColor" className="informationicon" > - + {' '} Info -
+ ); } - return {content}; + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{content}; } + + static propTypes = { + item: PropTypes.any.isRequired, + onShowModel: PropTypes.func.isRequired, + isResultExpanded: PropTypes.any, + }; + + static defaultProps = { + isResultExpanded: null, + }; } -TabContent.propTypes = { - item: PropTypes.any.isRequired, - onShowModel: PropTypes.func.isRequired, - expandResults: PropTypes.any, -}; export default TabContent; diff --git a/src/Components/Grid/TabContent.test.js b/src/Components/Grid/TabContent.test.js index 0c85c8e..b480da5 100644 --- a/src/Components/Grid/TabContent.test.js +++ b/src/Components/Grid/TabContent.test.js @@ -10,7 +10,7 @@ test('Should contain formated time', () => { failureMessages: [], }; const { container } = render( - , + {}} />, ); expect( container.textContent.indexOf( @@ -27,7 +27,7 @@ test('Should call function on icon click', () => { failureMessages: [], }; const { container } = render( - , + , ); fireEvent.click(container.lastChild.firstChild); expect(FakeFun).toHaveBeenCalledTimes(1); diff --git a/src/Components/Grid/TabHeading.js b/src/Components/Grid/TabHeading.js index 27a5e66..63cd35f 100644 --- a/src/Components/Grid/TabHeading.js +++ b/src/Components/Grid/TabHeading.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; class TabHeading extends Component { constructor(props) { super(props); + // eslint-disable-next-line react/state-in-constructor this.state = { resultSummary: { numFailedTests: this.props.item.numFailedTests ?? 0, @@ -13,14 +14,17 @@ class TabHeading extends Component { numPendingTests: this.props.item.numPendingTests ?? 0, numTodoTests: this.props.item.numTodoTests ?? 0, }, - isChecked: this.props.expandResults, + isChecked: this.props.isResultExpanded, }; } + toggleChange = () => { this.setState({ + // eslint-disable-next-line react/no-access-state-in-setstate isChecked: !this.state.isChecked, }); }; + render() { return (
@@ -28,8 +32,8 @@ class TabHeading extends Component { type="checkbox" id={`elem_${this.props.item.id}`} checked={this.state.isChecked} - onChange={this.toggleChange} className="togglerCheckBox" + onChange={this.toggleChange} />
); } + + static propTypes = { + item: PropTypes.any.isRequired, + onShowModel: PropTypes.func.isRequired, + isResultExpanded: PropTypes.bool, + }; + + static defaultProps = { + isResultExpanded: false, + }; } -TabHeading.propTypes = { - item: PropTypes.any.isRequired, - onShowModel: PropTypes.func.isRequired, - expandResults: PropTypes.any, -}; export default TabHeading; diff --git a/src/Components/Grid/TabHeading.test.js b/src/Components/Grid/TabHeading.test.js index ccf0549..6abeac3 100644 --- a/src/Components/Grid/TabHeading.test.js +++ b/src/Components/Grid/TabHeading.test.js @@ -3,18 +3,14 @@ import React from 'react'; import TabHeading from './TabHeading'; import { render, fireEvent } from '@testing-library/react'; describe('Expand result tab heading test', () => { - test('Should be expanded when expandResults is true', () => { + test('Should be expanded when isResultExpanded is true', () => { const item = { title: 'Test parent', id: '1', children: [{ title: 'Test child', id: '2' }], }; const { container } = render( - , + {}} />, ); expect(container.firstChild.firstChild.checked).toBe(true); expect( @@ -22,24 +18,20 @@ describe('Expand result tab heading test', () => { ).toEqual('block'); }); - test('Should be able to toggle when expandResults is true', () => { + test('Should be able to toggle when isResultExpanded is true', () => { const item = { title: 'Test parent', id: '1', children: [{ title: 'Test child', id: '2' }], }; const { container } = render( - , + {}} />, ); fireEvent.click(container.firstChild.firstChild); expect(container.firstChild.firstChild.checked).toBe(false); }); - test('Should be collapsed when expandResults is false', () => { + test('Should be collapsed when isResultExpanded is false', () => { const item = { title: 'Test parent', id: '1', @@ -47,15 +39,15 @@ describe('Expand result tab heading test', () => { }; const { container } = render( , + onShowModel={() => {}} + />, ); expect(container.firstChild.firstChild.checked).toBe(false); }); - test('Should be able to toggle when expandResults is false', () => { + test('Should be able to toggle when isResultExpanded is false', () => { const item = { title: 'Test parent', id: '1', @@ -63,16 +55,16 @@ describe('Expand result tab heading test', () => { }; const { container } = render( , + onShowModel={() => {}} + />, ); fireEvent.click(container.firstChild.firstChild); expect(container.firstChild.firstChild.checked).toBe(true); }); - test('Should be collapsed when expandResults is null', () => { + test('Should be collapsed when isResultExpanded is null', () => { const item = { title: 'Test parent', id: '1', @@ -80,15 +72,15 @@ describe('Expand result tab heading test', () => { }; const { container } = render( , + onShowModel={() => {}} + />, ); expect(container.firstChild.firstChild.checked).toBe(false); }); - test('Should be collapsed when expandResults is undefined', () => { + test('Should be collapsed when isResultExpanded is undefined', () => { const item = { title: 'Test parent', id: '1', @@ -96,10 +88,10 @@ describe('Expand result tab heading test', () => { }; const { container } = render( , + onShowModel={() => {}} + />, ); expect(container.firstChild.firstChild.checked).toBe(false); }); diff --git a/src/Components/Header/Header.js b/src/Components/Header/Header.js index cacf5ed..3b8d784 100644 --- a/src/Components/Header/Header.js +++ b/src/Components/Header/Header.js @@ -1,40 +1,47 @@ import React, { Fragment, useState } from 'react'; import './Header.css'; import PropTypes from 'prop-types'; -export const Header = ({ menuStateChange, heading, hideMenu }) => { +export const Header = ({ menuStateChange, heading, isMenuHidden }) => { const [toggle, setToggle] = useState(false); function toggleButton() { - if (!toggle) { - setToggle(true); - menuStateChange('open'); - } else { + if (toggle) { setToggle(false); menuStateChange('close'); + } else { + setToggle(true); + menuStateChange('open'); } } + return (
- {hideMenu ? ( - + {isMenuHidden ? ( + // eslint-disable-next-line react/jsx-no-useless-fragment + <> ) : ( - + <> toggleButton()} />