Преглед изворни кода

Create unit tests for new components

Also includes new unit tests for old components which had none.
Sergiu Miclea пре 8 година
родитељ
комит
deefe373b6
32 измењених фајлова са 1004 додато и 46 уклоњено
  1. 1 1
      package.json
  2. 1 1
      private/cypress/integration/6 - users and projects/2 - Add a new user as a member.js
  3. 1 1
      private/cypress/integration/6 - users and projects/3 - Add existing user as a member.js
  4. 2 0
      src/components/atoms/AutocompleteInput/AutocompleteInput.jsx
  5. 61 0
      src/components/atoms/AutocompleteInput/test.jsx
  6. 5 1
      src/components/atoms/ToggleButtonBar/ToggleButtonBar.jsx
  7. 2 0
      src/components/molecules/AutocompleteDropdown/AutocompleteDropdown.jsx
  8. 2 0
      src/components/molecules/DropdownInput/DropdownInput.jsx
  9. 71 0
      src/components/molecules/DropdownInput/test.jsx
  10. 2 1
      src/components/molecules/EndpointField/EndpointField.jsx
  11. 6 5
      src/components/molecules/ProjectListItem/ProjectListItem.jsx
  12. 84 0
      src/components/molecules/ProjectListItem/test.jsx
  13. 6 1
      src/components/molecules/Table/Table.jsx
  14. 7 7
      src/components/molecules/UserListItem/UserListItem.jsx
  15. 45 0
      src/components/molecules/UserListItem/story.jsx
  16. 59 0
      src/components/molecules/UserListItem/test.jsx
  17. 6 1
      src/components/molecules/WizardOptionsField/WizardOptionsField.jsx
  18. 9 4
      src/components/organisms/EndpointDuplicateOptions/EndpointDuplicateOptions.jsx
  19. 74 0
      src/components/organisms/EndpointDuplicateOptions/test.jsx
  20. 14 6
      src/components/organisms/ProjectDetailsContent/ProjectDetailsContent.jsx
  21. 83 0
      src/components/organisms/ProjectDetailsContent/test.jsx
  22. 7 2
      src/components/organisms/ProjectMemberModal/ProjectMemberModal.jsx
  23. 148 0
      src/components/organisms/ProjectMemberModal/test.jsx
  24. 3 1
      src/components/organisms/ProjectModal/ProjectModal.jsx
  25. 74 0
      src/components/organisms/ProjectModal/test.jsx
  26. 13 9
      src/components/organisms/UserDetailsContent/UserDetailsContent.jsx
  27. 60 0
      src/components/organisms/UserDetailsContent/test.jsx
  28. 3 1
      src/components/organisms/UserModal/UserModal.jsx
  29. 107 0
      src/components/organisms/UserModal/test.jsx
  30. 2 2
      src/components/organisms/WizardPageContent/WizardPageContent.jsx
  31. 39 0
      src/components/organisms/WizardPageContent/test.jsx
  32. 7 2
      src/utils/TestWrapper.js

+ 1 - 1
package.json

@@ -103,4 +103,4 @@
     "webpack-blocks-happypack": "^0.1.3",
     "webpack-blocks-split-vendor": "^0.2.1"
   }
-}
+}

+ 1 - 1
private/cypress/integration/6 - users and projects/2 - Add a new user as a member.js

@@ -63,7 +63,7 @@ describe('Adds a new user as a member to the project', () => {
     cy.route({ url: '**/users', method: 'POST' }).as('addUser')
     cy.route({ url: '**/roles/**', method: 'PUT' }).as('addRole')
     cy.route({ url: '**/role_assignments**', method: 'GET' }).as('getRoles')
-    cy.get('button[data-test-id="projectModal-addButton"]').contains('Add Member').click()
+    cy.get('button[data-test-id="pmModal-addButton"]').contains('Add Member').click()
     cy.wait('@addUser')
     cy.wait('@addRole')
     cy.wait('@getRoles')

+ 1 - 1
private/cypress/integration/6 - users and projects/3 - Add existing user as a member.js

@@ -58,7 +58,7 @@ describe('Adds existing user as a member to the project', () => {
     cy.server()
     cy.route({ url: '**/roles/**', method: 'PUT' }).as('addRoles')
     cy.route({ url: '**/role_assignments**', method: 'GET' }).as('getRoles')
-    cy.get('button[data-test-id="projectModal-addButton"]').click()
+    cy.get('button[data-test-id="pmModal-addButton"]').click()
     cy.wait('@addRoles')
     cy.wait('@getRoles')
     cy.get('div[data-test-id="pdContent-roles-cypress-member-user"]').should('contain', '_member_, admin')

+ 2 - 0
src/components/atoms/AutocompleteInput/AutocompleteInput.jsx

@@ -98,6 +98,7 @@ class AutocompleteInput extends React.Component<Props, State> {
         }}
       >
         <TextInput
+          data-test-id="acInput-text"
           disabled={this.props.disabled}
           value={this.props.value}
           onChange={e => { this.props.onChange(e.target.value) }}
@@ -111,6 +112,7 @@ class AutocompleteInput extends React.Component<Props, State> {
           onBlur={() => { this.setState({ textInputFocus: false }) }}
         />
         <Arrow
+          data-test-id="acInput-arrow"
           disabled={this.props.disabled}
           dangerouslySetInnerHTML={{ __html: arrowImage }}
           onClick={this.props.onClick}

+ 61 - 0
src/components/atoms/AutocompleteInput/test.jsx

@@ -0,0 +1,61 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import React from 'react'
+import sinon from 'sinon'
+import { shallow } from 'enzyme'
+import TW from '../../../utils/TestWrapper'
+import AutocompleteInput from '.'
+
+type Props = {
+  value: string,
+  customRef?: (ref: HTMLElement) => void,
+  innerRef?: (ref: HTMLElement) => void,
+  onChange: (value: string) => void,
+  onClick?: () => void,
+  disabled?: boolean,
+  width?: number,
+  large?: boolean,
+  onFocus?: () => void,
+  highlight?: boolean,
+}
+
+const wrap = (props: Props) => new TW(shallow(
+  <AutocompleteInput {...props} />
+), 'acInput')
+
+describe('AutocompleteInput Component', () => {
+  it('renders input with correct data', () => {
+    let wrapper = wrap({
+      value: 'value',
+      onChange: () => { },
+    })
+
+    expect(wrapper.find('text').prop('embedded')).toBe(true)
+    expect(wrapper.find('text').prop('value')).toBe('value')
+  })
+
+  it('dispatches click', () => {
+    let onClick = sinon.spy()
+    let wrapper = wrap({
+      value: 'value',
+      onChange: () => { },
+      onClick,
+    })
+    wrapper.find('arrow').click()
+    expect(onClick.calledOnce).toBe(true)
+  })
+})

+ 5 - 1
src/components/atoms/ToggleButtonBar/ToggleButtonBar.jsx

@@ -57,6 +57,7 @@ type Props = {
   selectedValue?: string,
   onChange?: (item: ItemType) => void,
   className?: string,
+  'data-test-id'?: string,
 }
 @observer
 class ToggleButtonBar extends React.Component<Props> {
@@ -66,7 +67,10 @@ class ToggleButtonBar extends React.Component<Props> {
     }
 
     return (
-      <Wrapper className={this.props.className}>
+      <Wrapper
+        data-test-id={this.props['data-test-id'] || 'toggleButtonBar-wrapper'}
+        className={this.props.className}
+      >
         {this.props.items.map(item => {
           return (
             <Item

+ 2 - 0
src/components/molecules/AutocompleteDropdown/AutocompleteDropdown.jsx

@@ -106,6 +106,7 @@ type Props = {
   width?: number,
   dimNullValue?: boolean,
   highlight?: boolean,
+  'data-test-id'?: string,
 }
 type State = {
   showDropdownList: boolean,
@@ -417,6 +418,7 @@ class AutocompleteDropdown extends React.Component<Props, State> {
 
     return (
       <Wrapper
+        data-test-id={this.props['data-test-id'] || 'acDropdown-wrapper'}
         className={this.props.className}
         onMouseDown={() => { this.itemMouseDown = true }}
         onMouseUp={() => { this.itemMouseDown = false }}

+ 2 - 0
src/components/molecules/DropdownInput/DropdownInput.jsx

@@ -74,6 +74,7 @@ class DropdownInput extends React.Component<Props, State> {
           secondary
           disabled={this.props.disabled}
           arrowImage={arrowImage}
+          data-test-id="ddInput-link"
         />
         <TextInput
           embedded
@@ -83,6 +84,7 @@ class DropdownInput extends React.Component<Props, State> {
           onChange={e => { this.props.onInputChange(e.target.value) }}
           placeholder={this.props.placeholder}
           disabled={this.props.disabled}
+          data-test-id="ddInput-text"
         />
       </Wrapper>
     )

+ 71 - 0
src/components/molecules/DropdownInput/test.jsx

@@ -0,0 +1,71 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { shallow } from 'enzyme'
+import TW from '../../../utils/TestWrapper'
+import DropdownInput from '.'
+
+type ItemType = {
+  label: string,
+  value: string,
+  [string]: any,
+}
+type Props = {
+  items: ItemType[],
+  selectedItem: string,
+  onItemChange: (item: ItemType) => void,
+  inputValue: string,
+  onInputChange: (value: string) => void,
+  placeholder?: string,
+  highlight?: boolean,
+  disabled?: boolean,
+}
+
+const wrap = (props: Props) => new TW(shallow(
+  <DropdownInput {...props} />
+), 'ddInput')
+
+const items = [
+  { label: 'Item 1', value: 'item-1' },
+  { label: 'Item 2', value: 'item-2' },
+]
+
+describe('DropdownInput Component', () => {
+  it('renders link with correct data', () => {
+    let wrapper = wrap({
+      items,
+      selectedItem: 'item-2',
+      onItemChange: () => { },
+      inputValue: 'input-value',
+      onInputChange: () => { },
+    })
+    expect(wrapper.find('link').prop('items')[1].value).toBe(items[1].value)
+    expect(wrapper.find('link').prop('selectedItem')).toBe('item-2')
+  })
+
+  it('renders text input with correct data', () => {
+    let wrapper = wrap({
+      items,
+      selectedItem: 'item-2',
+      onItemChange: () => { },
+      inputValue: 'input-value',
+      onInputChange: () => { },
+    })
+    expect(wrapper.find('text').prop('embedded')).toBe(true)
+    expect(wrapper.find('text').prop('value')).toBe('input-value')
+  })
+})

+ 2 - 1
src/components/molecules/EndpointField/EndpointField.jsx

@@ -76,6 +76,7 @@ type Props = {
   noSelectionMessage?: string,
   noItemsMessage?: string,
   selectedItems?: string[],
+  'data-test-id'?: string,
 }
 @observer
 class Field extends React.Component<Props> {
@@ -283,7 +284,7 @@ class Field extends React.Component<Props> {
 
   render() {
     return (
-      <Wrapper className={this.props.className}>
+      <Wrapper data-test-id={this.props['data-test-id'] || 'endpointField-wrapper'} className={this.props.className}>
         {this.renderLabel()}
         {this.renderInput()}
       </Wrapper>

+ 6 - 5
src/components/molecules/ProjectListItem/ProjectListItem.jsx

@@ -110,19 +110,19 @@ class ProjectListItem extends React.Component<Props> {
         <Content onClick={this.props.onClick} data-test-id={`${testName}-content`}>
           <Image />
           <Title>
-            <TitleLabel>{this.props.item.name}</TitleLabel>
-            <Subtitle>{this.props.item.description}</Subtitle>
+            <TitleLabel data-test-id={`${testName}-name`}>{this.props.item.name}</TitleLabel>
+            <Subtitle data-test-id={`${testName}-description`}>{this.props.item.description}</Subtitle>
           </Title>
           <Body>
             <Data percentage={33}>
               <ItemLabel>Members</ItemLabel>
-              <ItemValue>
+              <ItemValue data-test-id={`${testName}-members`}>
                 {this.props.getMembers(this.props.item.id)}
               </ItemValue>
             </Data>
             <Data percentage={33}>
               <ItemLabel>Enabled</ItemLabel>
-              <ItemValue>
+              <ItemValue data-test-id={`${testName}-enabled`}>
                 {this.props.item.enabled ? 'Yes' : 'No'}
               </ItemValue>
             </Data>
@@ -133,8 +133,9 @@ class ProjectListItem extends React.Component<Props> {
                 hollow
                 onMouseDown={e => { e.stopPropagation() }}
                 onMouseUp={e => { e.stopPropagation() }}
-                onClick={e => { e.stopPropagation(); this.props.onSwitchProjectClick(this.props.item.id) }}
+                onClick={e => { if (e) e.stopPropagation(); this.props.onSwitchProjectClick(this.props.item.id) }}
                 disabled={isCurrentProject}
+                data-test-id={`${testName}-currentButton`}
               >{isCurrentProject ? 'Current' : 'Switch'}</Button>
             </Data>
           </Body>

+ 84 - 0
src/components/molecules/ProjectListItem/test.jsx

@@ -0,0 +1,84 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import TW from '../../../utils/TestWrapper'
+import ProjectListItem from '.'
+import type { Project } from '../../../types/Project'
+
+type Props = {
+  item: Project,
+  onClick: () => void,
+  getMembers: (projectId: string) => number,
+  isCurrentProject: (projectId: string) => boolean,
+  onSwitchProjectClick: (projectId: string) => void,
+}
+
+const wrap = (props: Props) => new TW(shallow(
+  <ProjectListItem {...props} />
+), 'plItem')
+
+const item: Project = {
+  id: 'p_id',
+  name: 'p_name',
+  description: 'p_description',
+  enabled: true,
+}
+describe('ProjectListItem Component', () => {
+  it('renders with correct data', () => {
+    let wrapper = wrap({
+      item,
+      onClick: () => { },
+      getMembers: () => 3,
+      isCurrentProject: () => true,
+      onSwitchProjectClick: () => { },
+    })
+    expect(wrapper.findText('name')).toBe(item.name)
+    expect(wrapper.findText('description')).toBe(item.description)
+    expect(wrapper.findText('members')).toBe('3')
+    expect(wrapper.findText('enabled')).toBe('Yes')
+    expect(wrapper.findText('currentButton', false, true)).toBe('Current')
+  })
+
+  it('dispatches click', () => {
+    let onClick = sinon.spy()
+    let wrapper = wrap({
+      item,
+      onClick,
+      getMembers: () => 3,
+      isCurrentProject: () => true,
+      onSwitchProjectClick: () => { },
+    })
+    wrapper.find('content').click()
+    expect(onClick.calledOnce).toBe(true)
+  })
+
+  it('dispatches switch project click', () => {
+    let onSwitchProjectClick = sinon.spy()
+    let wrapper = wrap({
+      item,
+      onClick: () => { },
+      getMembers: () => 3,
+      isCurrentProject: () => true,
+      onSwitchProjectClick,
+    })
+    wrapper.find('currentButton').click()
+    expect(onSwitchProjectClick.calledOnce).toBe(true)
+    expect(onSwitchProjectClick.args[0][0]).toBe('p_id')
+  })
+})

+ 6 - 1
src/components/molecules/Table/Table.jsx

@@ -107,6 +107,7 @@ type Props = {
   noItemsLabel?: string,
   bodyStyle?: any,
   headerStyle?: any,
+  'data-test-id'?: string,
 }
 @observer
 class Table extends React.Component<Props> {
@@ -175,7 +176,11 @@ class Table extends React.Component<Props> {
 
   render() {
     return (
-      <Wrapper className={this.props.className} secondary={this.props.useSecondaryStyle}>
+      <Wrapper
+        data-test-id={this.props['data-test-id'] || 'table-wrapper'}
+        className={this.props.className}
+        secondary={this.props.useSecondaryStyle}
+      >
         {this.renderHeader()}
         {this.renderItems()}
         {this.renderNoItems()}

+ 7 - 7
src/components/molecules/UserListItem/UserListItem.jsx

@@ -98,32 +98,32 @@ type Props = {
 }
 const testName = 'ulItem'
 @observer
-class EndpointListItem extends React.Component<Props> {
+class UserListItem extends React.Component<Props> {
   render() {
     return (
       <Wrapper>
-        <Content onClick={this.props.onClick}>
+        <Content data-test-id={`${testName}-content`} onClick={this.props.onClick}>
           <Image />
           <Title>
             <TitleLabel data-test-id={`${testName}-name`}>{this.props.item.name}</TitleLabel>
-            <Subtitle>{this.props.item.description}</Subtitle>
+            <Subtitle data-test-id={`${testName}-description`}>{this.props.item.description}</Subtitle>
           </Title>
           <Body>
             <Data percentage={45}>
               <ItemLabel>Email</ItemLabel>
-              <ItemValue>
+              <ItemValue data-test-id={`${testName}-email`}>
                 {this.props.item.email || '-'}
               </ItemValue>
             </Data>
             <Data percentage={35}>
               <ItemLabel>Primary Project</ItemLabel>
-              <ItemValue>
+              <ItemValue data-test-id={`${testName}-project`}>
                 {this.props.getProjectName(this.props.item.project_id)}
               </ItemValue>
             </Data>
             <Data percentage={20}>
               <ItemLabel>Enabled</ItemLabel>
-              <ItemValue>
+              <ItemValue data-test-id={`${testName}-enabled`}>
                 {this.props.item.enabled ? 'Yes' : 'No'}
               </ItemValue>
             </Data>
@@ -134,4 +134,4 @@ class EndpointListItem extends React.Component<Props> {
   }
 }
 
-export default EndpointListItem
+export default UserListItem

+ 45 - 0
src/components/molecules/UserListItem/story.jsx

@@ -0,0 +1,45 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import UserListItem from '.'
+
+let user = {
+  id: 'id',
+  name: 'User Name',
+  description: 'user description',
+  email: 'user@email.com',
+  project_id: 'project_id',
+  enabled: true,
+  project: { name: '', id: '' },
+}
+class Wrapper extends React.Component<{}> {
+  render() {
+    return (
+      <UserListItem
+        item={user}
+        getProjectName={() => 'project name'}
+        onClick={() => { }}
+      />
+    )
+  }
+}
+
+storiesOf('UserListItem', module)
+  .add('default', () => (
+    <Wrapper />
+  ))

+ 59 - 0
src/components/molecules/UserListItem/test.jsx

@@ -0,0 +1,59 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import TW from '../../../utils/TestWrapper'
+import type { User } from '../../../types/User'
+import UserListItem from '.'
+
+type Props = {
+  item: User,
+  onClick: () => void,
+  getProjectName: (projectId: ?string) => string,
+}
+
+const wrap = (props: Props) => new TW(shallow(
+  <UserListItem {...props} />
+), 'ulItem')
+
+const user = {
+  id: 'id',
+  name: 'User Name',
+  description: 'user description',
+  email: 'user@email.com',
+  project_id: 'project_id',
+  enabled: true,
+  project: { name: '', id: '' },
+}
+describe('UserListItem Component', () => {
+  it('renders with correct data', () => {
+    let wrapper = wrap({ item: user, onClick: () => { }, getProjectName: id => `project ${id || ''}` })
+    expect(wrapper.findText('name')).toBe(user.name)
+    expect(wrapper.findText('description')).toBe(user.description)
+    expect(wrapper.findText('email')).toBe(user.email)
+    expect(wrapper.findText('project')).toBe('project project_id')
+    expect(wrapper.findText('enabled')).toBe('Yes')
+  })
+
+  it('dispatches click', () => {
+    let onClick = sinon.spy()
+    let wrapper = wrap({ item: user, onClick, getProjectName: () => '' })
+    wrapper.find('content').click()
+    expect(onClick.calledOnce).toBe(true)
+  })
+})

+ 6 - 1
src/components/molecules/WizardOptionsField/WizardOptionsField.jsx

@@ -71,6 +71,7 @@ type Props = {
   required: boolean,
   width?: number,
   skipNullValue?: boolean,
+  'data-test-id'?: string,
 }
 @observer
 class WizardOptionsField extends React.Component<Props> {
@@ -235,7 +236,11 @@ class WizardOptionsField extends React.Component<Props> {
     }
 
     return (
-      <Wrapper type={this.props.type} className={this.props.className}>
+      <Wrapper
+        data-test-id={this.props['data-test-id'] || 'wOptionsField-wrapper'}
+        type={this.props.type}
+        className={this.props.className}
+      >
         {this.renderLabel()}
         {field}
       </Wrapper>

+ 9 - 4
src/components/organisms/EndpointDuplicateOptions/EndpointDuplicateOptions.jsx

@@ -73,7 +73,7 @@ const Buttons = styled.div`
   justify-content: space-between;
   width: 100%;
 `
-const WizardOptionsFieldStyled = styled(WizardOptionsField) `
+const WizardOptionsFieldStyled = styled(WizardOptionsField)`
   width: 319px;
   justify-content: space-between;
 `
@@ -87,6 +87,7 @@ type Props = {
 type State = {
   selectedProjectId: string,
 }
+const testName = 'edOptions'
 @observer
 class EndpointDuplicateOptions extends React.Component<Props, State> {
   componentWillMount() {
@@ -105,8 +106,8 @@ class EndpointDuplicateOptions extends React.Component<Props, State> {
 
   renderDuplicating() {
     return (
-      <Loading>
-        <StatusImage loading />
+      <Loading data-test-id={`${testName}-loading`}>
+        <StatusImage data loading />
         <Message>
           <Title>Duplicating Endpoint</Title>
           <Subtitle>Please wait ...</Subtitle>
@@ -121,6 +122,7 @@ class EndpointDuplicateOptions extends React.Component<Props, State> {
         <Image />
         <Form>
           <WizardOptionsFieldStyled
+            data-test-id={`${testName}-field-project`}
             name="duplicate_to_project"
             type="string"
             enum={this.props.projects}
@@ -134,7 +136,10 @@ class EndpointDuplicateOptions extends React.Component<Props, State> {
         </Form>
         <Buttons>
           <Button secondary onClick={this.props.onCancelClick}>Cancel</Button>
-          <Button onClick={() => { this.props.onDuplicateClick(this.state.selectedProjectId) }}>Duplicate</Button>
+          <Button
+            data-test-id={`${testName}-duplicateButton`}
+            onClick={() => { this.props.onDuplicateClick(this.state.selectedProjectId) }}
+          >Duplicate</Button>
         </Buttons>
       </Options>
     )

+ 74 - 0
src/components/organisms/EndpointDuplicateOptions/test.jsx

@@ -0,0 +1,74 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import TW from '../../../utils/TestWrapper'
+import type { Project } from '../../../types/Project'
+import EndpointDuplicateOptions from '.'
+
+type Props = {
+  projects: Project[],
+  selectedProjectId: string,
+  duplicating: boolean,
+  onCancelClick: () => void,
+  onDuplicateClick: (projectId: string) => void,
+}
+
+const wrap = (props: Props) => new TW(shallow(<EndpointDuplicateOptions {...props} />), 'edOptions')
+const projects: Project[] = [
+  { id: 'project-1', name: 'Project 1' },
+  { id: 'project-2', name: 'Project 2' },
+]
+describe('EndpointDuplicateOptions Component', () => {
+  it('renders projects', () => {
+    let wrapper = wrap({
+      projects,
+      selectedProjectId: 'project-2',
+      duplicating: false,
+      onCancelClick: () => { },
+      onDuplicateClick: () => { },
+    })
+    expect(wrapper.find('field-project').prop('enum')[1].name).toBe(projects[1].name)
+    expect(wrapper.find('field-project').prop('value')).toBe('project-2')
+    expect(wrapper.find('loading').length).toBe(0)
+  })
+
+  it('dispatches duplicate', () => {
+    let onDuplicateClick = sinon.spy()
+    let wrapper = wrap({
+      projects,
+      selectedProjectId: 'project-2',
+      duplicating: false,
+      onCancelClick: () => { },
+      onDuplicateClick,
+    })
+    wrapper.find('duplicateButton').click()
+    expect(onDuplicateClick.args[0][0]).toBe('project-2')
+  })
+
+  it('renders loading', () => {
+    let wrapper = wrap({
+      projects,
+      selectedProjectId: 'project-2',
+      duplicating: true,
+      onCancelClick: () => { },
+      onDuplicateClick: () => { },
+    })
+    expect(wrapper.find('loading').length).toBe(1)
+  })
+})

+ 14 - 6
src/components/organisms/ProjectDetailsContent/ProjectDetailsContent.jsx

@@ -111,6 +111,7 @@ type State = {
   showRemoveUserAlert: boolean,
   showDeleteProjectAlert: boolean,
 }
+const testName = 'pdContent'
 @observer
 class ProjectDetailsContent extends React.Component<Props, State> {
   selectedUser: ?User
@@ -202,7 +203,7 @@ class ProjectDetailsContent extends React.Component<Props, State> {
       <Info>
         <Field>
           <Label>Name</Label>
-          {this.renderValue(project.name)}
+          {this.renderValue(project.name, 'name')}
         </Field>
         <Field>
           <Label>Description</Label>
@@ -210,7 +211,7 @@ class ProjectDetailsContent extends React.Component<Props, State> {
         </Field>
         <Field>
           <Label>ID</Label>
-          {this.renderValue(project.id)}
+          {this.renderValue(project.id, 'id')}
         </Field>
         <Field>
           <Label>Enabled</Label>
@@ -256,7 +257,7 @@ class ProjectDetailsContent extends React.Component<Props, State> {
           href={`#/user/${user.id}`}
         >{user.name}</UserName>,
         <DropdownLink
-          data-test-id={`pdContent-roles-${user.name}`}
+          data-test-id={`${testName}-roles-${user.name}`}
           width="214px"
           getLabel={() => userRoles.length > 0 ? userRoles.map(r => r.label).join(', ') : 'No roles'}
           selectedItems={userRoles.map(r => r.value)}
@@ -272,7 +273,7 @@ class ProjectDetailsContent extends React.Component<Props, State> {
         />,
         <UserColumn disabled={!user.enabled}>{user.enabled ? 'Enabled' : 'Disabled'}</UserColumn>,
         <DropdownLink
-          data-test-id={`pdContent-actions-${user.name}`}
+          data-test-id={`${testName}-actions-${user.name}`}
           noCheckmark
           width="82px"
           items={userActions}
@@ -290,6 +291,7 @@ class ProjectDetailsContent extends React.Component<Props, State> {
 
     return (
       <TableStyled
+        data-test-id={`${testName}-members`}
         header={['Member', 'Roles', 'Status', '']}
         items={rows}
         noItemsLabel="No members available!"
@@ -298,8 +300,14 @@ class ProjectDetailsContent extends React.Component<Props, State> {
     )
   }
 
-  renderValue(value: string) {
-    return value !== '-' ? <CopyValue value={value} maxWidth="90%" /> : <Value>{value}</Value>
+  renderValue(value: string, dataTestId: string) {
+    return value !== '-' ? (
+      <CopyValue
+        data-test-id={`${testName}-${dataTestId}`}
+        value={value}
+        maxWidth="90%"
+      />
+    ) : <Value>{value}</Value>
   }
 
   render() {

+ 83 - 0
src/components/organisms/ProjectDetailsContent/test.jsx

@@ -0,0 +1,83 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { shallow } from 'enzyme'
+import TW from '../../../utils/TestWrapper'
+import type { Project, Role, RoleAssignment } from '../../../types/Project'
+import type { User } from '../../../types/User'
+import ProjectDetailsContent from '.'
+
+type Props = {
+  project: ?Project,
+  loading: boolean,
+  users: User[],
+  usersLoading: boolean,
+  deleteDisabled: boolean,
+  roleAssignments: RoleAssignment[],
+  roles: Role[],
+  loggedUserId: string,
+}
+const wrap = (props: Props) => new TW(shallow(
+  <ProjectDetailsContent
+    onAddMemberClick={() => { }}
+    onDeleteConfirmation={() => { }}
+    onEditProjectClick={() => { }}
+    onEnableUser={() => { }}
+    onRemoveUser={() => { }}
+    onUserRoleChange={() => { }}
+    {...props}
+  />
+), 'pdContent')
+const projects: Project[] = [
+  { id: 'project-1', name: 'Project 1' },
+  { id: 'project-2', name: 'Project 2' },
+]
+const users: User[] = [
+  { id: 'user-1', name: 'User 1', email: 'email1', project: projects[0] },
+  { id: 'user-2', name: 'User 2', email: 'email2', project: projects[1] },
+]
+const roles: Role[] = [
+  { id: 'role-1', name: 'Role 1' },
+  { id: 'role-2', name: 'Role 2' },
+]
+const roleAssignments: RoleAssignment[] = [
+  { user: users[0], role: roles[0], scope: { project: projects[0] } },
+  { user: users[1], role: roles[1], scope: { project: projects[0] } },
+]
+describe('ProjectDetailsContent Component', () => {
+  it('renders info', () => {
+    let wrapper = wrap({
+      project: projects[0],
+      loading: false,
+      users,
+      usersLoading: false,
+      deleteDisabled: false,
+      roleAssignments,
+      roles,
+      loggedUserId: 'user-1',
+    })
+    expect(wrapper.find('name').prop('value')).toBe('Project 1')
+    expect(wrapper.find('id').prop('value')).toBe('project-1')
+    let rows = wrapper.find('members').prop('items')
+    expect(rows[0][0].props['data-test-id']).toBe('pdContent-users-User 1')
+    expect(rows[0][1].props.selectedItems.length).toBe(1)
+    expect(rows[0][1].props.selectedItems[0]).toBe('role-1')
+    expect(rows[1][0].props['data-test-id']).toBe('pdContent-users-User 2')
+    expect(rows[1][1].props.selectedItems.length).toBe(1)
+    expect(rows[1][1].props.selectedItems[0]).toBe('role-2')
+  })
+})

+ 7 - 2
src/components/organisms/ProjectMemberModal/ProjectMemberModal.jsx

@@ -99,7 +99,7 @@ type State = {
   selectedRolesExisting: string[],
   selectedRolesNew: string[],
 }
-
+const testName = 'pmModal'
 @observer
 class ProjectMemberModal extends React.Component<Props, State> {
   state = {
@@ -204,6 +204,7 @@ class ProjectMemberModal extends React.Component<Props, State> {
     }]
     return (
       <ToggleButtonBarStyled
+        data-test-id={`${testName}-formToggle`}
         items={items}
         selectedValue={this.state.isNew ? 'new' : 'existing'}
         onChange={item => { this.setState({ isNew: item.value === 'new' }) }}
@@ -224,6 +225,8 @@ class ProjectMemberModal extends React.Component<Props, State> {
 
     return (
       <Field
+        data-test-id={`${testName}-roles`}
+        key="roles"
         name="role(s)"
         type="array"
         onChange={roleId => {
@@ -249,6 +252,7 @@ class ProjectMemberModal extends React.Component<Props, State> {
   renderField(field: FieldType, value: any, onChange: (value: any) => void) {
     return (
       <FieldStyled
+        data-test-id={`${testName}-field-${field.name}`}
         key={field.name}
         name={field.name}
         type={field.type || 'string'}
@@ -330,6 +334,7 @@ class ProjectMemberModal extends React.Component<Props, State> {
             <Asterisk marginLeft="8px" />
           </FormLabel>
           <AutocompleteDropdown
+            data-test-id={`${testName}-users`}
             items={users}
             disabled={this.props.loading}
             selectedItem={this.state.selectedUser ? this.state.selectedUser.id : ''}
@@ -372,7 +377,7 @@ class ProjectMemberModal extends React.Component<Props, State> {
               large
               disabled={this.props.loading}
               onClick={() => { this.handleAddClick() }}
-              data-test-id="projectModal-addButton"
+              data-test-id={`${testName}-addButton`}
             >Add Member</Button>
           </Buttons>
         </Wrapper>

+ 148 - 0
src/components/organisms/ProjectMemberModal/test.jsx

@@ -0,0 +1,148 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import TW from '../../../utils/TestWrapper'
+import type { User } from '../../../types/User'
+import type { Project, Role } from '../../../types/Project'
+import ProjectMemberModal from '.'
+
+type Props = {
+  loading: boolean,
+  users: User[],
+  projects: Project[],
+  onRequestClose: () => void,
+  onAddClick: (user: User, isNew: boolean, roles: Role[]) => void,
+  roles: Role[],
+}
+const wrap = (props: Props) => new TW(shallow(<ProjectMemberModal {...props} />), 'pmModal')
+const users: User[] = [
+  { id: 'user-1', name: 'User 1', email: '', project: { id: '', name: '' } },
+  { id: 'user-2', name: 'User 2', email: '', project: { id: '', name: '' } },
+]
+const projects: Project[] = [
+  { id: 'project-1', name: 'Project 1' },
+  { id: 'project-2', name: 'Project 2' },
+]
+const roles: Role[] = [
+  { id: 'role-1', name: 'Role 1' },
+  { id: 'role-2', name: 'Role 2' },
+  { id: 'role-3', name: 'Role 3' },
+]
+describe('ProjectMemberModal Component', () => {
+  it('renders existing user form', () => {
+    let wrapper = wrap({
+      loading: false,
+      users,
+      projects,
+      roles,
+      onRequestClose: () => { },
+      onAddClick: () => { },
+    })
+    expect(wrapper.find('users').prop('items')[1].value).toBe(users[1].id)
+    expect(wrapper.find('roles').prop('items')[1].value).toBe(roles[1].id)
+    expect(wrapper.find('users').prop('highlight')).toBe(false)
+    expect(wrapper.find('roles').prop('highlight')).toBe(false)
+    expect(wrapper.find('users').prop('disabled')).toBe(false)
+    expect(wrapper.find('roles').prop('disabled')).toBe(false)
+  })
+
+  it('highlights required fields in existing user form', () => {
+    let wrapper = wrap({
+      loading: false,
+      users,
+      projects,
+      roles,
+      onRequestClose: () => { },
+      onAddClick: () => { },
+    })
+    expect(wrapper.find('users').length).toBe(1)
+    wrapper.find('addButton').click()
+    expect(wrapper.find('users').prop('highlight')).toBe(true)
+    expect(wrapper.find('roles').prop('highlight')).toBe(true)
+  })
+
+  it('renders new user form and highlights required', () => {
+    let wrapper = wrap({
+      loading: false,
+      users,
+      projects,
+      roles,
+      onRequestClose: () => { },
+      onAddClick: () => { },
+    })
+    wrapper.find('formToggle').simulate('change', { value: 'new' })
+    expect(wrapper.find('users').length).toBe(0)
+    expect(wrapper.find('field-username').prop('highlight')).toBe(false)
+    expect(wrapper.find('field-description').prop('highlight')).toBe(false)
+    expect(wrapper.find('field-Primary Project').prop('highlight')).toBe(false)
+    expect(wrapper.find('roles').prop('highlight')).toBe(false)
+    expect(wrapper.find('field-password').prop('highlight')).toBe(false)
+    expect(wrapper.find('field-confirm_password').prop('highlight')).toBe(false)
+    expect(wrapper.find('field-Email').prop('highlight')).toBe(false)
+    wrapper.find('addButton').click()
+    expect(wrapper.find('field-username').prop('highlight')).toBe(true)
+    expect(wrapper.find('field-description').prop('highlight')).toBe(false)
+    expect(wrapper.find('field-Primary Project').prop('highlight')).toBe(false)
+    expect(wrapper.find('roles').prop('highlight')).toBe(true)
+    expect(wrapper.find('field-password').prop('highlight')).toBe(true)
+    expect(wrapper.find('field-confirm_password').prop('highlight')).toBe(false)
+    expect(wrapper.find('field-Email').prop('highlight')).toBe(false)
+  })
+
+  it('dispatches add click with correct data', () => {
+    let onAddClick = sinon.spy()
+    let wrapper = wrap({
+      loading: false,
+      users,
+      projects,
+      roles,
+      onRequestClose: () => { },
+      onAddClick,
+    })
+    wrapper.find('formToggle').simulate('change', { value: 'new' })
+    wrapper.find('field-username').simulate('change', 'new-username')
+    wrapper.find('roles').simulate('change', 'role-2')
+    wrapper.find('roles').simulate('change', 'role-1')
+    wrapper.find('roles').simulate('change', 'role-2')
+    wrapper.find('roles').simulate('change', 'role-3')
+    wrapper.find('field-password').simulate('change', 'new-password')
+    wrapper.find('field-confirm_password').simulate('change', 'new-password')
+    wrapper.find('addButton').click()
+    let userArg = onAddClick.args[0][0]
+    let rolesArg: Role[] = onAddClick.args[0][2]
+    expect(userArg.name).toBe('new-username')
+    expect(userArg.password).toBe('new-password')
+    expect(rolesArg.length).toBe(2)
+    expect(rolesArg[0].id).toBe('role-1')
+    expect(rolesArg[1].id).toBe('role-3')
+  })
+
+  it('disabled on loading', () => {
+    let wrapper = wrap({
+      loading: true,
+      users,
+      projects,
+      roles,
+      onRequestClose: () => { },
+      onAddClick: () => { },
+    })
+    expect(wrapper.find('users').prop('disabled')).toBe(true)
+    expect(wrapper.find('roles').prop('disabled')).toBe(true)
+  })
+})

+ 3 - 1
src/components/organisms/ProjectModal/ProjectModal.jsx

@@ -66,7 +66,7 @@ type State = {
   highlightFieldNames: string[],
   description: string,
 }
-
+const testName = 'projectModal'
 @observer
 class ProjectModal extends React.Component<Props, State> {
   componentWillMount() {
@@ -117,6 +117,7 @@ class ProjectModal extends React.Component<Props, State> {
   renderField(field: FieldType, value: any, onChange: (value: any) => void) {
     return (
       <Field
+        data-test-id={`${testName}-field-${field.name}`}
         key={field.name}
         name={field.name}
         type={field.type || 'string'}
@@ -176,6 +177,7 @@ class ProjectModal extends React.Component<Props, State> {
               onClick={this.props.onRequestClose}
             >Cancel</Button>
             <Button
+              data-test-id={`${testName}-updateButton`}
               large
               disabled={this.props.loading}
               onClick={() => { this.handleUpdateClick() }}

+ 74 - 0
src/components/organisms/ProjectModal/test.jsx

@@ -0,0 +1,74 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import TW from '../../../utils/TestWrapper'
+import type { Project } from '../../../types/Project'
+import ProjectModal from '.'
+
+type Props = {
+  project?: ?Project,
+  isNewProject?: boolean,
+  loading: boolean,
+  onRequestClose: () => void,
+  onUpdateClick: (project: Project) => void,
+}
+
+const wrap = (props: Props) => new TW(shallow(<ProjectModal {...props} />), 'projectModal')
+
+describe('ProjectModal Component', () => {
+  it('doesn\'t dispatch click if project name is not filled', () => {
+    let onUpdateClick = sinon.spy()
+    let wrapper = wrap({
+      isNewProject: true,
+      loading: false,
+      onRequestClose: () => { },
+      onUpdateClick,
+    })
+    expect(wrapper.findText('updateButton', false, true)).toBe('New Project')
+    wrapper.find('updateButton').click()
+    expect(onUpdateClick.called).toBe(false)
+    expect(wrapper.find('field-project_name').prop('highlight')).toBe(true)
+  })
+
+  it('dispatches click if project is filled', () => {
+    let onUpdateClick = sinon.spy()
+    let wrapper = wrap({
+      isNewProject: false,
+      project: { id: 'project', name: 'Project Name' },
+      loading: false,
+      onRequestClose: () => { },
+      onUpdateClick,
+    })
+    expect(wrapper.findText('updateButton', false, true)).toBe('Update Project')
+    wrapper.find('updateButton').click()
+    expect(onUpdateClick.called).toBe(true)
+  })
+
+  it('has disabled fields on loading', () => {
+    let wrapper = wrap({
+      isNewProject: false,
+      project: { id: 'project', name: 'Project Name' },
+      loading: true,
+      onRequestClose: () => { },
+      onUpdateClick: () => { },
+    })
+    expect(wrapper.find('updateButton').prop('disabled')).toBe(true)
+    expect(wrapper.find('field-project_name').prop('disabled')).toBe(true)
+  })
+})

+ 13 - 9
src/components/organisms/UserDetailsContent/UserDetailsContent.jsx

@@ -94,6 +94,7 @@ type Props = {
 type State = {
   showDeleteConfirmation: boolean,
 }
+const testName = 'udContent'
 @observer
 class UserDetailsContent extends React.Component<Props, State> {
   state = {
@@ -144,12 +145,9 @@ class UserDetailsContent extends React.Component<Props, State> {
 
   renderUserProjects(projects: { label: string, id: string }[]) {
     return projects.map((project, i) => (
-      <span>
+      <span key={project.id}>
         {project.label ? (
-          <Link
-            key={project.id}
-            href={`#/project/${project.id}`}
-          >
+          <Link data-test-id={`${testName}-project-${project.id}`} href={`#/project/${project.id}`}>
             {project.label}
           </Link>
         ) : project.id}
@@ -178,7 +176,7 @@ class UserDetailsContent extends React.Component<Props, State> {
       <Info>
         <Field>
           <Label>Name</Label>
-          {this.renderValue(user.name)}
+          {this.renderValue(user.name, 'name')}
         </Field>
         <Field>
           <Label>Description</Label>
@@ -186,7 +184,7 @@ class UserDetailsContent extends React.Component<Props, State> {
         </Field>
         <Field>
           <Label>ID</Label>
-          {this.renderValue(user.id)}
+          {this.renderValue(user.id, 'id')}
         </Field>
         <Field>
           <Label>Email</Label>
@@ -208,8 +206,14 @@ class UserDetailsContent extends React.Component<Props, State> {
     )
   }
 
-  renderValue(value: string) {
-    return value !== '-' ? <CopyValue value={value} maxWidth="90%" /> : <Value>{value}</Value>
+  renderValue(value: string, dataTestId?: string) {
+    return value !== '-' ? (
+      <CopyValue
+        data-test-id={`${testName}-${dataTestId || ''}`}
+        value={value}
+        maxWidth="90%"
+      />
+    ) : <Value>{value}</Value>
   }
 
   render() {

+ 60 - 0
src/components/organisms/UserDetailsContent/test.jsx

@@ -0,0 +1,60 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { shallow } from 'enzyme'
+import TW from '../../../utils/TestWrapper'
+import type { Project } from '../../../types/Project'
+import type { User } from '../../../types/User'
+import UserDetailsContent from '.'
+
+type Props = {
+  user: ?User,
+  loading: boolean,
+  projects: Project[],
+  userProjects: Project[],
+  isLoggedUser: boolean,
+}
+const wrap = (props: Props) => new TW(shallow(
+  <UserDetailsContent
+    onEditClick={() => { }}
+    onUpdatePasswordClick={() => { }}
+    onDeleteConfirmation={() => { }}
+    {...props}
+  />
+), 'udContent')
+
+const projects: Project[] = [
+  { id: 'project-1', name: 'Project 1' },
+  { id: 'project-2', name: 'Project 2' },
+]
+const user: User = { id: 'user-1', name: 'User 1', email: 'email1', project: projects[0] }
+
+describe('UserDetailsContent Component', () => {
+  it('renders info', () => {
+    let wrapper = wrap({
+      user,
+      projects,
+      userProjects: projects,
+      loading: false,
+      isLoggedUser: false,
+    })
+    expect(wrapper.find('name').prop('value')).toBe('User 1')
+    expect(wrapper.find('id').prop('value')).toBe('user-1')
+    expect(wrapper.find('project-', true).at(0).text(true)).toBe('Project 1')
+    expect(wrapper.find('project-', true).at(1).text(true)).toBe('Project 2')
+  })
+})

+ 3 - 1
src/components/organisms/UserModal/UserModal.jsx

@@ -78,7 +78,7 @@ type State = {
   confirmPassword: string,
   description: string,
 }
-
+const testName = 'userModal'
 @observer
 class UserModal extends React.Component<Props, State> {
   componentWillMount() {
@@ -172,6 +172,7 @@ class UserModal extends React.Component<Props, State> {
 
     return (
       <Field
+        data-test-id={`${testName}-field-${field.name}`}
         key={field.name}
         name={field.name}
         type={field.type || 'string'}
@@ -271,6 +272,7 @@ class UserModal extends React.Component<Props, State> {
               onClick={this.props.onRequestClose}
             >Cancel</Button>
             <Button
+              data-test-id={`${testName}-updateButton`}
               large
               disabled={this.props.loading}
               onClick={() => { this.handleUpdateClick() }}

+ 107 - 0
src/components/organisms/UserModal/test.jsx

@@ -0,0 +1,107 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import TW from '../../../utils/TestWrapper'
+import type { Project } from '../../../types/Project'
+import type { User } from '../../../types/User'
+import UserModal from '.'
+
+type Props = {
+  user?: User,
+  isLoggedUser?: boolean,
+  loading: boolean,
+  isNewUser?: boolean,
+  projects: Project[],
+  editPassword?: boolean,
+  onRequestClose: () => void,
+  onUpdateClick: (user: User) => void,
+}
+
+const wrap = (props: Props) => new TW(shallow(<UserModal {...props} />), 'userModal')
+const projects: Project[] = [
+  { id: 'project-1', name: 'Project 1' },
+  { id: 'project-2', name: 'Project 2' },
+]
+describe('UserModal Component', () => {
+  it('doesn\'t dispatch click if required fields are not filled', () => {
+    let onUpdateClick = sinon.spy()
+    let wrapper = wrap({
+      isNewUser: true,
+      isLoggedUser: false,
+      loading: false,
+      projects,
+      onRequestClose: () => { },
+      onUpdateClick,
+    })
+    expect(wrapper.findText('updateButton', false, true)).toBe('New User')
+    wrapper.find('updateButton').click()
+    expect(onUpdateClick.called).toBe(false)
+    expect(wrapper.find('field-username').prop('highlight')).toBe(true)
+    expect(wrapper.find('field-new_password').prop('highlight')).toBe(true)
+  })
+
+  it('dispatches click if project is filled', () => {
+    let onUpdateClick = sinon.spy()
+    let wrapper = wrap({
+      user: { id: 'user-1', name: 'User 1', email: 'email', project: projects[0] },
+      isNewUser: false,
+      isLoggedUser: false,
+      loading: false,
+      projects,
+      onRequestClose: () => { },
+      onUpdateClick,
+    })
+    expect(wrapper.findText('updateButton', false, true)).toBe('Update User')
+    wrapper.find('updateButton').click()
+    expect(onUpdateClick.called).toBe(true)
+  })
+
+  it('has disabled fields on loading', () => {
+    let wrapper = wrap({
+      user: { id: 'user-1', name: 'User 1', email: 'email', project: projects[0] },
+      isNewUser: false,
+      isLoggedUser: false,
+      loading: true,
+      projects,
+      onRequestClose: () => { },
+      onUpdateClick: () => { },
+    })
+    expect(wrapper.find('updateButton').prop('disabled')).toBe(true)
+    expect(wrapper.find('field-username').prop('disabled')).toBe(true)
+    expect(wrapper.find('field-new_password').length).toBe(0)
+    expect(wrapper.find('field-confirm_password').length).toBe(0)
+  })
+
+  it('renders change password form', () => {
+    let wrapper = wrap({
+      user: { id: 'user-1', name: 'User 1', email: 'email', project: projects[0] },
+      isNewUser: false,
+      isLoggedUser: false,
+      loading: true,
+      projects,
+      editPassword: true,
+      onRequestClose: () => { },
+      onUpdateClick: () => { },
+    })
+
+    expect(wrapper.findText('updateButton', false, true)).toBe('Change Password')
+    expect(wrapper.find('field-new_password').length).toBe(1)
+    expect(wrapper.find('field-confirm_password').length).toBe(1)
+  })
+})

+ 2 - 2
src/components/organisms/WizardPageContent/WizardPageContent.jsx

@@ -122,7 +122,7 @@ type State = {
   useAdvancedOptions: boolean,
   timezone: TimezoneValue,
 }
-
+const testName = 'wpContent'
 @observer
 class WizardPageContent extends React.Component<Props, State> {
   constructor() {
@@ -269,7 +269,7 @@ class WizardPageContent extends React.Component<Props, State> {
       title += ` ${this.props.type.charAt(0).toUpperCase() + this.props.type.substr(1)}`
     }
 
-    return <Header>{title}</Header>
+    return <Header data-test-id={`${testName}-header`}>{title}</Header>
   }
 
   renderBody() {

+ 39 - 0
src/components/organisms/WizardPageContent/test.jsx

@@ -0,0 +1,39 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { shallow } from 'enzyme'
+import TW from '../../../utils/TestWrapper'
+import WizardPageContent from '.'
+
+const wrap = (props: any) => new TW(shallow(
+  <WizardPageContent
+    wizardData={{}}
+    onContentRef={() => { }}
+    {...props}
+  />
+), 'wpContent')
+
+describe('WizardPageContent Component', () => {
+  it('renders wizard type page', () => {
+    const wrapper = wrap({
+      page: { id: 'type', title: 'Wizard Type' },
+      type: 'replica',
+    })
+    expect(wrapper.findText('header')).toBe('Wizard Type Replica')
+    expect(wrapper.shallow.find('WizardType').prop('selected')).toBe('replica')
+  })
+})

+ 7 - 2
src/utils/TestWrapper.js

@@ -42,6 +42,10 @@ export default class TestWrapper {
     this.shallow.simulate('click')
   }
 
+  text(render?: boolean) {
+    return render ? this.shallow.render().text() : this.shallow.text()
+  }
+
   find(id: string, isPartialId?: boolean) {
     const actualId = this.baseId ? `${this.baseId}-${id}` : id
     let tw = new TestWrapper(this.shallow.findWhere(w =>
@@ -51,9 +55,10 @@ export default class TestWrapper {
     return tw
   }
 
-  findText(id: string, isHostComponent?: boolean) {
+  findText(id: string, isHostComponent?: boolean, render?: boolean) {
     const wrapper = this.find(id).shallow
-    return isHostComponent ? wrapper.text() : wrapper.dive().text()
+    const text = (component: any) => render ? component.render().text() : component.text()
+    return isHostComponent ? text(wrapper) : text(wrapper.dive())
   }
 
   debug() {