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: [],
- });
-};