diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.jsx b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.jsx index 7bfcdc0675..70d4c8f450 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.jsx +++ b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.jsx @@ -29,20 +29,19 @@ class SelectTargetsDropdown extends Component { constructor (props) { super(props); - this.mounted = true; - this.state = { isEmpty: false, isLoadingTargets: false, moreInfoTarget: null, query: '', targets: [], - wrapperHeight: 0, }; } - componentDidMount () { + componentWillMount () { this.fetchTargets(); + this.mounted = true; + this.wrapperHeight = 0; return false; } @@ -106,19 +105,6 @@ class SelectTargetsDropdown extends Component { return false; } - const { target_type: targetType } = moreInfoTarget; - - if (targetType.toLowerCase() === 'labels') { - return Kolide.getLabelHosts(moreInfoTarget.id) - .then((hosts) => { - this.setState({ - moreInfoTarget: { ...moreInfoTarget, hosts }, - }); - - return false; - }); - } - this.setState({ moreInfoTarget }); return false; @@ -126,12 +112,10 @@ class SelectTargetsDropdown extends Component { } onBackToResults = () => { - this.setState({ - moreInfoTarget: null, - }); + this.setState({ moreInfoTarget: null }); } - fetchTargets = (query, selectedTargets = this.props.selectedTargets) => { + fetchTargets = (query = '', selectedTargets = this.props.selectedTargets) => { const { onFetchTargets } = this.props; if (!this.mounted) { @@ -142,26 +126,25 @@ class SelectTargetsDropdown extends Component { return Kolide.targets.loadAll(query, formatSelectedTargetsForApi(selectedTargets)) .then((response) => { - const { - targets, - } = response; + const { targets } = response; + const isEmpty = targets.length === 0; if (!this.mounted) { return false; } - if (targets.length === 0) { + if (isEmpty) { // We don't want the lib's default "No Results" so we fake it targets.push({}); - - this.setState({ isEmpty: true }); - } else { - this.setState({ isEmpty: false }); } onFetchTargets(query, response); - this.setState({ isLoadingTargets: false, targets }); + this.setState({ + isEmpty, + isLoadingTargets: false, + targets, + }); return query; }) diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.tests.jsx b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.tests.jsx new file mode 100644 index 0000000000..597567e973 --- /dev/null +++ b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.tests.jsx @@ -0,0 +1,169 @@ +import React from 'react'; +import expect, { createSpy, restoreSpies } from 'expect'; +import { mount } from 'enzyme'; +import nock from 'nock'; +import { noop } from 'lodash'; + +import SelectTargetsDropdown from 'components/forms/fields/SelectTargetsDropdown'; +import Test from 'test'; + +describe('SelectTargetsDropdown - component', () => { + const defaultProps = { + disabled: false, + label: 'Select Targets', + onFetchTargets: noop, + onSelect: noop, + selectedTargets: [], + targetsCount: 0, + }; + const DefaultComponent = mount(); + + afterEach(() => nock.cleanAll()); + + it('sets default state', () => { + Test.Mocks.targetMock(); + + expect(DefaultComponent.state()).toEqual({ + isEmpty: false, + isLoadingTargets: false, + moreInfoTarget: null, + query: '', + targets: [], + }); + }); + + describe('rendering', () => { + beforeEach(() => Test.Mocks.targetMock()); + + it('renders', () => { + expect(DefaultComponent.length).toEqual(1, 'Expected component to render'); + }); + + it('renders the SelectTargetsInput', () => { + const SelectTargetsInput = DefaultComponent.find('SelectTargetsInput'); + + expect(SelectTargetsInput.length).toEqual(1, 'Expected SelectTargetsInput to render'); + }); + + it('renders a label when passed as a prop', () => { + const noLabelProps = { ...defaultProps, label: undefined }; + const ComponentWithoutLabel = mount(); + const Label = DefaultComponent.find('.target-select__label'); + const NoLabel = ComponentWithoutLabel.find('.target-select__label'); + + expect(Label.length).toEqual(1, 'Expected label to render'); + expect(NoLabel.length).toEqual(0, 'Expected label to not render'); + }); + + it('renders the error when passed as a prop', () => { + const errorProps = { ...defaultProps, error: "You can't do this!" }; + const ErrorComponent = mount(); + const Error = ErrorComponent.find('.target-select__label--error'); + const NoError = DefaultComponent.find('.target-select__label--error'); + + expect(Error.length).toEqual(1, 'Expected error to render'); + expect(NoError.length).toEqual(0, 'Expected error to not render'); + }); + + it('renders the target count', () => { + const targetCountProps = { ...defaultProps, targetsCount: 10 }; + const TargetCountComponent = mount(); + + expect(DefaultComponent.text()).toInclude('0 unique hosts'); + expect(TargetCountComponent.text()).toInclude('10 unique hosts'); + }); + }); + + describe('#fetchTargets', () => { + const apiResponseWithTargets = { + targets: { + hosts: [], + labels: [Test.Stubs.labelStub], + }, + }; + const apiResponseWithoutTargets = { + targets: { + hosts: [], + labels: [], + }, + }; + const defaultSelectedTargets = { hosts: [], labels: [] }; + const defaultParams = { + query: '', + selected: defaultSelectedTargets, + }; + const expectedApiClientResponseWithTargets = { + targets: [{ ...Test.Stubs.labelStub, target_type: 'labels' }], + }; + + afterEach(() => restoreSpies()); + + it('calls the api', () => { + nock.cleanAll(); + + const request = Test.Mocks.targetMock(defaultParams, apiResponseWithTargets); + const Component = mount(); + const node = Component.node; + + return node.fetchTargets() + .then(() => { + expect(request.isDone()).toEqual(true); + }); + }); + + it('calls the onFetchTargets prop', () => { + nock.cleanAll(); + + const onFetchTargets = createSpy(); + const props = { ...defaultProps, onFetchTargets }; + const Component = mount(); + const node = Component.node; + + Test.Mocks.targetMock(defaultParams, apiResponseWithTargets); + + return node.fetchTargets() + .then(() => { + expect(onFetchTargets).toHaveBeenCalledWith('', expectedApiClientResponseWithTargets); + }); + }); + + it('does not call the onFetchTargets prop when the component is not mounted', () => { + const onFetchTargets = createSpy(); + const props = { ...defaultProps, onFetchTargets }; + const Component = mount(); + const node = Component.node; + + node.mounted = false; + + expect(node.fetchTargets()).toEqual(false); + expect(onFetchTargets).toNotHaveBeenCalled(); + }); + + it('sets state correctly when no targets are returned', () => { + const Component = mount(); + const node = Component.node; + + Test.Mocks.targetMock(defaultParams, apiResponseWithoutTargets); + + return node.fetchTargets() + .then(() => { + expect(Component.state('isEmpty')).toEqual(true); + expect(Component.state('targets')).toEqual([{}]); + expect(Component.state('isLoadingTargets')).toEqual(false); + }); + }); + + it('returns the query', () => { + const query = 'select * from users'; + const Component = mount(); + const node = Component.node; + + Test.Mocks.targetMock(defaultParams); + + return node.fetchTargets(query) + .then((q) => { + expect(q).toEqual(query); + }); + }); + }); +}); diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/SelectTargetsMenu.tests.jsx b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/SelectTargetsMenu.tests.jsx new file mode 100644 index 0000000000..c6bb29070c --- /dev/null +++ b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/SelectTargetsMenu.tests.jsx @@ -0,0 +1,93 @@ +import React, { PropTypes } from 'react'; +import expect, { createSpy, restoreSpies } from 'expect'; +import { mount } from 'enzyme'; +import { noop } from 'lodash'; + +import SelectTargetsMenuWrapper from 'components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu'; +import Test from 'test'; + +const DummyOption = (props) => { + return ( +
{props.children}
+ ); +}; + +DummyOption.propTypes = { children: PropTypes.node }; + +describe('SelectTargetsMenu - component', () => { + afterEach(() => restoreSpies()); + + let onMoreInfoClick = noop; + const moreInfoTarget = undefined; + const handleBackToResults = noop; + const defaultProps = { + focusedOption: undefined, + instancePrefix: '', + onFocus: noop, + onOptionRef: noop, + onSelect: noop, + optionComponent: DummyOption, + }; + + describe('rendering', () => { + it('renders', () => { + const SelectTargetsMenu = SelectTargetsMenuWrapper(onMoreInfoClick, moreInfoTarget, handleBackToResults); + const Component = mount(); + + expect(Component.length).toEqual(1); + }); + + it('renders no target text', () => { + const SelectTargetsMenu = SelectTargetsMenuWrapper(onMoreInfoClick, moreInfoTarget, handleBackToResults); + const Component = mount(); + const componentText = Component.text(); + + expect(componentText).toInclude('Unable to find any matching labels.'); + expect(componentText).toInclude('Unable to find any matching hosts.'); + }); + + it('renders a target option component for each target', () => { + const SelectTargetsMenu = SelectTargetsMenuWrapper(onMoreInfoClick, moreInfoTarget, handleBackToResults); + const options = [Test.Stubs.labelStub, Test.Stubs.hostStub]; + const props = { ...defaultProps, options }; + const Component = mount(); + const TargetOption = Component.find('TargetOption'); + + expect(TargetOption.length).toEqual(options.length); + + expect(options).toInclude(TargetOption.first().prop('target')); + expect(options).toInclude(TargetOption.last().prop('target')); + }); + }); + + describe('clicking a target', () => { + it('calls the onMoreInfoClick function', () => { + const spy = createSpy(); + onMoreInfoClick = (t) => { + return () => spy(t); + }; + const SelectTargetsMenu = SelectTargetsMenuWrapper(onMoreInfoClick, moreInfoTarget, handleBackToResults); + const options = [Test.Stubs.labelStub]; + const props = { ...defaultProps, options }; + const Component = mount(); + const TargetOption = Component.find('TargetOption'); + + TargetOption.find('.target-option__target-content').simulate('click'); + + expect(spy).toHaveBeenCalledWith(Test.Stubs.labelStub); + }); + + it('calls the onSelect prop when the add button is clicked', () => { + const spy = createSpy(); + const SelectTargetsMenu = SelectTargetsMenuWrapper(onMoreInfoClick, moreInfoTarget, handleBackToResults); + const options = [Test.Stubs.labelStub]; + const props = { ...defaultProps, onSelect: spy, options }; + const Component = mount(); + const TargetOption = Component.find('TargetOption'); + + TargetOption.find('.target-option__add-btn').simulate('click'); + + expect(spy).toHaveBeenCalled(); + }); + }); +}); diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/TargetDetails.tests.jsx b/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/TargetDetails.tests.jsx new file mode 100644 index 0000000000..8714fb766f --- /dev/null +++ b/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/TargetDetails.tests.jsx @@ -0,0 +1,121 @@ +import React from 'react'; +import expect, { createSpy, restoreSpies } from 'expect'; +import { mount } from 'enzyme'; + +import TargetDetails from 'components/forms/fields/SelectTargetsDropdown/TargetDetails'; +import Test from 'test'; + +describe('TargetDetails - component', () => { + afterEach(() => restoreSpies()); + + const defaultProps = { target: Test.Stubs.labelStub }; + + describe('rendering', () => { + it('does not render without a target', () => { + const Component = mount(); + + expect(Component.html()).toNotExist(); + }); + + it('renders when there is a target', () => { + const Component = mount(); + + expect(Component.length).toEqual(1); + }); + + describe('when the target is a host', () => { + it('renders target information', () => { + const target = { + display_text: 'display_text', + host_mac: 'host_mac', + host_ip_address: 'host_ip_address', + memory: 1074000000, // 1 GB memory + osquery_version: 'osquery_version', + os_version: 'os_version', + platform: 'platform', + status: 'status', + }; + const Component = mount(); + const componentText = Component.text(); + + expect(componentText).toInclude(target.display_text); + expect(componentText).toInclude(target.host_mac); + expect(componentText).toInclude(target.host_ip_address); + expect(componentText).toInclude('1.00 GB'); + expect(componentText).toInclude(target.osquery_version); + expect(componentText).toInclude(target.os_version); + expect(componentText).toInclude(target.platform); + expect(componentText).toInclude(target.status); + }); + + it('renders a success check icon when the target is online', () => { + const target = { ...Test.Stubs.hostStub, status: 'online' }; + const Component = mount(); + const Icon = Component.find('Icon'); + const onlineIcon = Icon.find('.host-target__icon--online'); + const offlineIcon = Icon.find('.host-target__icon--offline'); + + expect(onlineIcon.length).toEqual(1, 'Expected the online icon to render'); + expect(offlineIcon.length).toEqual(0, 'Expected the offline icon to not render'); + }); + + it('renders a offline icon when the target is offline', () => { + const target = { ...Test.Stubs.hostStub, status: 'offline' }; + const Component = mount(); + const Icon = Component.find('Icon'); + const onlineIcon = Icon.find('.host-target__icon--online'); + const offlineIcon = Icon.find('.host-target__icon--offline'); + + expect(onlineIcon.length).toEqual(0, 'Expected the online icon to not render'); + expect(offlineIcon.length).toEqual(1, 'Expected the offline icon to render'); + }); + }); + + describe('when the target is a label', () => { + const target = { + ...Test.Stubs.labelStub, + count: 10, + description: 'target description', + display_text: 'display_text', + label_type: 0, + online: 10, + query: 'query', + }; + const Component = mount(); + + it('renders the label data', () => { + const componentText = Component.text(); + + expect(componentText).toInclude('100% ONLINE'); + expect(componentText).toInclude(target.display_text); + expect(componentText).toInclude(target.description); + }); + + it('renders a read-only AceEditor', () => { + const AceEditor = Component.find('ReactAce'); + + expect(AceEditor.prop('readOnly')).toEqual(true); + }); + }); + }); + + it('calls the handleBackToResults prop when the back button is clicked', () => { + const labelSpy = createSpy(); + const labelProps = { ...defaultProps, handleBackToResults: labelSpy }; + const LabelComponent = mount(); + const LabelBackButton = LabelComponent.find('.label-target__back'); + + const hostSpy = createSpy(); + const hostProps = { target: Test.Stubs.hostStub, handleBackToResults: hostSpy }; + const HostComponent = mount(); + const HostBackButton = HostComponent.find('.host-target__back'); + + LabelBackButton.simulate('click'); + + expect(labelSpy).toHaveBeenCalled(); + + HostBackButton.simulate('click'); + + expect(hostSpy).toHaveBeenCalled(); + }); +}); diff --git a/frontend/components/forms/packs/PackForm/PackForm.tests.jsx b/frontend/components/forms/packs/PackForm/PackForm.tests.jsx index 7fe7d15a9c..53d914a7c0 100644 --- a/frontend/components/forms/packs/PackForm/PackForm.tests.jsx +++ b/frontend/components/forms/packs/PackForm/PackForm.tests.jsx @@ -4,11 +4,11 @@ import { mount } from 'enzyme'; import { noop } from 'lodash'; import { fillInFormInput } from 'test/helpers'; -import targetStub from 'test/target_stub'; +import targetMock from 'test/target_mock'; import PackForm from './index'; describe('PackForm - component', () => { - beforeEach(targetStub); + beforeEach(targetMock); afterEach(restoreSpies); it('renders the base error', () => { diff --git a/frontend/components/forms/queries/QueryForm/QueryForm.tests.jsx b/frontend/components/forms/queries/QueryForm/QueryForm.tests.jsx index b6f82ceb93..98b0de6347 100644 --- a/frontend/components/forms/queries/QueryForm/QueryForm.tests.jsx +++ b/frontend/components/forms/queries/QueryForm/QueryForm.tests.jsx @@ -4,7 +4,7 @@ import { mount } from 'enzyme'; import { noop } from 'lodash'; import { fillInFormInput } from 'test/helpers'; -import targetStub from 'test/target_stub'; +import targetMock from 'test/target_mock'; import QueryForm from './index'; const query = { @@ -16,7 +16,7 @@ const query = { const queryText = 'SELECT * FROM users'; describe('QueryForm - component', () => { - beforeEach(targetStub); + beforeEach(targetMock); afterEach(restoreSpies); it('renders the base error', () => { diff --git a/frontend/test/index.js b/frontend/test/index.js new file mode 100644 index 0000000000..51656d8a7d --- /dev/null +++ b/frontend/test/index.js @@ -0,0 +1,10 @@ +import helpers from 'test/helpers'; +import stubs from 'test/stubs'; +import mocks from 'test/mocks'; +import targetMock from 'test/target_mock'; + +export default { + Helpers: helpers, + Mocks: { ...mocks, targetMock }, + Stubs: stubs, +}; diff --git a/frontend/test/target_mock.js b/frontend/test/target_mock.js new file mode 100644 index 0000000000..39d7763d5e --- /dev/null +++ b/frontend/test/target_mock.js @@ -0,0 +1,21 @@ +import nock from 'nock'; + +const defaultParams = { + selected: { + hosts: [], + labels: [], + }, +}; +const defaultResponse = { + targets: { + hosts: [], + labels: [], + }, +}; + + +export default (params = defaultParams, response = defaultResponse, responseStatus = 200) => { + return nock('http://localhost:8080') + .post('/api/v1/kolide/targets', JSON.stringify(params)) + .reply(responseStatus, response); +}; diff --git a/frontend/test/target_stub.js b/frontend/test/target_stub.js deleted file mode 100644 index 21c3369658..0000000000 --- a/frontend/test/target_stub.js +++ /dev/null @@ -1,16 +0,0 @@ -import nock from 'nock'; - -const defaultParams = { - selected: { - hosts: [], - labels: [], - }, -}; - -export default (params = defaultParams) => { - nock('http://localhost:8080') - .post('/api/v1/kolide/targets', JSON.stringify(params)) - .reply(200, { - targets: [], - }); -};