From f4eca0a228859217397a6a3b5cbf6497004a037e Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Sat, 3 Mar 2018 21:56:50 -0800 Subject: [PATCH] Convert MeasurementList to TypeScript and add tests --- ui/src/shared/components/DatabaseList.tsx | 4 - ...MeasurementList.js => MeasurementList.tsx} | 104 ++++++------ .../components/MeasurementList.test.tsx | 159 ++++++++++++++++++ 3 files changed, 212 insertions(+), 55 deletions(-) rename ui/src/shared/components/{MeasurementList.js => MeasurementList.tsx} (80%) create mode 100644 ui/test/shared/components/MeasurementList.test.tsx diff --git a/ui/src/shared/components/DatabaseList.tsx b/ui/src/shared/components/DatabaseList.tsx index d3d4aa19aa..413f4181c1 100644 --- a/ui/src/shared/components/DatabaseList.tsx +++ b/ui/src/shared/components/DatabaseList.tsx @@ -23,10 +23,6 @@ interface DatabaseListState { namespaces: Namespace[] } -export interface DatabaseListContext { - source: Source -} - const {shape, string} = PropTypes class DatabaseList extends PureComponent { diff --git a/ui/src/shared/components/MeasurementList.js b/ui/src/shared/components/MeasurementList.tsx similarity index 80% rename from ui/src/shared/components/MeasurementList.js rename to ui/src/shared/components/MeasurementList.tsx index 6b2175f6e5..e2dc2c7f9d 100644 --- a/ui/src/shared/components/MeasurementList.js +++ b/ui/src/shared/components/MeasurementList.tsx @@ -1,52 +1,56 @@ -import React, {PropTypes} from 'react' +import React, {PureComponent} from 'react' +import PropTypes from 'prop-types' import classnames from 'classnames' import _ from 'lodash' -import {showMeasurements} from 'shared/apis/metaQuery' -import showMeasurementsParser from 'shared/parsing/showMeasurements' +import {showMeasurements} from 'src/shared/apis/metaQuery' +import showMeasurementsParser from 'src/shared/parsing/showMeasurements' + +import {Query, Source} from 'src/types' import TagList from 'src/data_explorer/components/TagList' -import FancyScrollbar from 'shared/components/FancyScrollbar' +import FancyScrollbar from 'src/shared/components/FancyScrollbar' -const {func, shape, string} = PropTypes +interface Props { + query: Query + querySource: Source + onChooseTag: () => void + onGroupByTag: () => void + onToggleTagAcceptance: () => void + onChooseMeasurement: (measurement: string) => void +} -const MeasurementList = React.createClass({ - propTypes: { - query: shape({ - database: string, - measurement: string, - }).isRequired, - onChooseMeasurement: func.isRequired, - onChooseTag: func.isRequired, - onToggleTagAcceptance: func.isRequired, - onGroupByTag: func.isRequired, - querySource: shape({ - links: shape({ - proxy: string.isRequired, - }).isRequired, - }), - }, +interface State { + measurements: string[] + filterText: string +} - contextTypes: { +const {shape, string} = PropTypes + +class MeasurementList extends PureComponent { + constructor(props) { + super(props) + this.state = { + measurements: [], + filterText: '', + } + + this.handleEscape = this.handleEscape.bind(this) + this.handleFilterText = this.handleFilterText.bind(this) + this.handleAcceptReject = this.handleAcceptReject.bind(this) + } + + public static defaultProps: Partial = { + querySource: null, + } + + public static contextTypes = { source: shape({ links: shape({ proxy: string.isRequired, }).isRequired, }).isRequired, - }, - - getInitialState() { - return { - measurements: [], - filterText: '', - } - }, - - getDefaultProps() { - return { - querySource: null, - } - }, + } componentDidMount() { if (!this.props.query.database) { @@ -54,7 +58,7 @@ const MeasurementList = React.createClass({ } this._getMeasurements() - }, + } componentDidUpdate(prevProps) { const {query, querySource} = this.props @@ -71,14 +75,14 @@ const MeasurementList = React.createClass({ } this._getMeasurements() - }, + } handleFilterText(e) { e.stopPropagation() this.setState({ - filterText: this.refs.filterText.value, + filterText: e.target.value, }) - }, + } handleEscape(e) { if (e.key !== 'Escape') { @@ -89,12 +93,11 @@ const MeasurementList = React.createClass({ this.setState({ filterText: '', }) - }, + } - handleAcceptReject(e) { - e.stopPropagation() + handleAcceptReject() { this.props.onToggleTagAcceptance() - }, + } render() { return ( @@ -105,14 +108,13 @@ const MeasurementList = React.createClass({ ?
@@ -121,7 +123,7 @@ const MeasurementList = React.createClass({ {this.renderList()} ) - }, + } renderList() { if (!this.props.query.database) { @@ -193,7 +195,7 @@ const MeasurementList = React.createClass({ ) - }, + } _getMeasurements() { const {source} = this.context @@ -206,14 +208,14 @@ const MeasurementList = React.createClass({ const {errors, measurementSets} = showMeasurementsParser(resp.data) if (errors.length) { // TODO: display errors in the UI. - return console.error('InfluxDB returned error(s): ', errors) // eslint-disable-line no-console + return console.error('InfluxDB returned error(s): ', errors) } this.setState({ measurements: measurementSets[0].measurements, }) }) - }, -}) + } +} export default MeasurementList diff --git a/ui/test/shared/components/MeasurementList.test.tsx b/ui/test/shared/components/MeasurementList.test.tsx new file mode 100644 index 0000000000..e890dd6ee0 --- /dev/null +++ b/ui/test/shared/components/MeasurementList.test.tsx @@ -0,0 +1,159 @@ +import React from 'react' +import MeasurementList from 'src/shared/components/MeasurementList' +import {shallow} from 'enzyme' +import {query, source} from 'test/resources' + +const setup = (override = {}) => { + const props = { + query, + querySource: source, + onChooseTag: () => {}, + onGroupByTag: () => {}, + onToggleTagAcceptance: () => {}, + onChooseMeasurement: () => {}, + ...override, + } + + MeasurementList.prototype._getMeasurements = jest.fn(() => Promise.resolve()) + + const wrapper = shallow(, { + context: {source}, + }) + + const instance = wrapper.instance() as MeasurementList + + return { + props, + wrapper, + instance, + } +} + +describe('Shared.Components.MeasurementList', () => { + describe('rendering', () => { + it('renders to the page', () => { + const {wrapper} = setup() + + expect(wrapper.exists()).toBe(true) + }) + }) + + describe('lifecycle methods', () => { + describe('componentDidMount', () => { + it('does not fire getMeasurements if there is no database', () => { + const _getMeasurements = jest.fn() + const {instance} = setup({query: {...query, database: ''}}) + instance._getMeasurements = _getMeasurements + instance.componentDidMount() + expect(_getMeasurements).not.toHaveBeenCalled() + }) + + it('does fire _getMeasurements if there is a database', () => { + const _getMeasurements = jest.fn() + const {instance} = setup() + instance._getMeasurements = _getMeasurements + instance.componentDidMount() + expect(_getMeasurements).toHaveBeenCalled() + }) + }) + + describe('componentDidUpdate', () => { + it('does not fire getMeasurements if there is no database', () => { + const _getMeasurements = jest.fn() + const {instance, props} = setup({query: {...query, database: ''}}) + + instance._getMeasurements = _getMeasurements + instance.componentDidUpdate(props) + + expect(_getMeasurements).not.toHaveBeenCalled() + }) + + it('does not fire _getMeasurements if the database does not change and the sources are equal', () => { + const _getMeasurements = jest.fn() + const {instance, props} = setup() + + instance._getMeasurements = _getMeasurements + instance.componentDidUpdate(props) + + expect(_getMeasurements).not.toHaveBeenCalled() + }) + + it('it fires _getMeasurements if there is a change in database', () => { + const _getMeasurements = jest.fn() + const {instance, props} = setup() + + instance._getMeasurements = _getMeasurements + instance.componentDidUpdate({ + ...props, + query: {...query, database: 'diffDb'}, + }) + + expect(_getMeasurements).toHaveBeenCalled() + }) + + it('it calls _getMeasurements if there is a change in source', () => { + const _getMeasurements = jest.fn() + const {instance, props} = setup() + + instance._getMeasurements = _getMeasurements + instance.componentDidUpdate({ + ...props, + querySource: {...source, id: 'newSource'}, + }) + + expect(_getMeasurements).toHaveBeenCalled() + }) + }) + }) + + describe('instance methods', () => { + describe('handleFilterText', () => { + it('sets the filterText state to the event targets value', () => { + const {instance} = setup() + const value = 'spectacs' + const stopPropagation = () => {} + const event = {target: {value}, stopPropagation} + + instance.handleFilterText(event) + + expect(instance.state.filterText).toBe(value) + }) + }) + + describe('handleEscape', () => { + it('resets fiterText when escape is pressed', () => { + const {instance} = setup() + const key = 'Escape' + const stopPropagation = () => {} + const event = {key, stopPropagation} + + instance.setState({filterText: 'foo'}) + expect(instance.state.filterText).toBe('foo') + + instance.handleEscape(event) + expect(instance.state.filterText).toBe('') + }) + + it('does not reset fiterText when escape is pressed', () => { + const {instance} = setup() + const key = 'Enter' + const stopPropagation = () => {} + const event = {key, stopPropagation} + + instance.setState({filterText: 'foo'}) + instance.handleEscape(event) + expect(instance.state.filterText).toBe('foo') + }) + }) + + describe('handleAcceptReject', () => { + it('it calls onToggleTagAcceptance', () => { + const onToggleTagAcceptance = jest.fn() + const {instance} = setup({onToggleTagAcceptance}) + + instance.handleAcceptReject() + expect(onToggleTagAcceptance).toHaveBeenCalledTimes(1) + }) + }) + }) +})