Просмотр исходного кода

Add new and improve existing unit tests

Contains:
- Using `data-test-id` prop instead of DOM diving.
- 'Flow' support.
- Additional unit tests and `Storybook` stories for new features.
Sergiu Miclea 8 лет назад
Родитель
Сommit
b024f66f5d
100 измененных файлов с 1129 добавлено и 597 удалено
  1. 1 1
      cypress.json
  2. 1 1
      package.json
  3. 4 4
      private/cypress/integration/migration/1 - Create Openstack Endpoint.js
  4. 2 2
      private/cypress/integration/migration/2 - Create VmWare Endpoint.js
  5. 5 5
      private/cypress/integration/migration/3 - Create VmWare Openstack Migration.js
  6. 2 2
      private/cypress/integration/migration/4 - Cancel first running migration.js
  7. 1 1
      private/cypress/integration/migration/6 - Delete first migration.js
  8. 2 2
      private/cypress/integration/migration/7 - Delete e2e Openstack endpoint.js
  9. 2 2
      private/cypress/integration/migration/8 - Delete e2e VmWare endpoint.js
  10. 3 3
      private/cypress/integration/replica/1 - Create Azure Endpoint.js
  11. 2 2
      private/cypress/integration/replica/2 - Create VmWare Endpoint.js
  12. 8 8
      private/cypress/integration/replica/3 - Create VmWare Azure Replica.js
  13. 2 2
      private/cypress/integration/replica/4 - Cancel first running replica.js
  14. 1 1
      private/cypress/integration/replica/5 - Cannot delete used endpoint.js
  15. 1 1
      private/cypress/integration/replica/6 - Delete first replica.js
  16. 2 2
      private/cypress/integration/replica/7 - Delete e2e Azure endpoint.js
  17. 2 2
      private/cypress/integration/replica/8 - Delete e2e VmWare endpoint.js
  18. 11 11
      private/cypress/integration/scheduler/Scheduler Operations.js
  19. 2 0
      src/components/atoms/Checkbox/index.jsx
  20. 1 1
      src/components/atoms/CopyValue/index.jsx
  21. 3 2
      src/components/atoms/CopyValue/test.jsx
  22. 1 1
      src/components/atoms/DropdownButton/index.jsx
  23. 3 2
      src/components/atoms/DropdownButton/test.jsx
  24. 10 2
      src/components/atoms/EndpointLogos/index.jsx
  25. 20 11
      src/components/atoms/EndpointLogos/test.jsx
  26. 25 0
      src/components/atoms/InfoIcon/story.jsx
  27. 1 1
      src/components/atoms/PasswordValue/index.jsx
  28. 15 11
      src/components/atoms/PasswordValue/test.jsx
  29. 1 1
      src/components/atoms/ProgressBar/index.jsx
  30. 9 4
      src/components/atoms/ProgressBar/test.jsx
  31. 2 2
      src/components/atoms/RadioInput/index.jsx
  32. 9 5
      src/components/atoms/RadioInput/test.jsx
  33. 7 1
      src/components/atoms/ReloadButton/index.jsx
  34. 9 5
      src/components/atoms/ReloadButton/test.jsx
  35. 1 1
      src/components/atoms/SearchButton/images/filter.js
  36. 1 1
      src/components/atoms/SearchButton/images/search.js
  37. 10 8
      src/components/atoms/SearchButton/index.jsx
  38. 8 0
      src/components/atoms/SearchButton/story.jsx
  39. 24 6
      src/components/atoms/SearchButton/test.jsx
  40. 6 2
      src/components/atoms/StatusIcon/test.jsx
  41. 6 1
      src/components/atoms/StatusImage/index.jsx
  42. 14 3
      src/components/atoms/StatusImage/test.jsx
  43. 11 9
      src/components/atoms/StatusPill/test.jsx
  44. 2 2
      src/components/atoms/Switch/index.jsx
  45. 24 20
      src/components/atoms/Switch/test.jsx
  46. 22 0
      src/components/atoms/TextArea/story.jsx
  47. 32 0
      src/components/atoms/TextArea/test.jsx
  48. 6 3
      src/components/atoms/TextInput/index.jsx
  49. 38 5
      src/components/atoms/TextInput/test.jsx
  50. 4 3
      src/components/atoms/ToggleButtonBar/index.jsx
  51. 15 14
      src/components/atoms/ToggleButtonBar/test.jsx
  52. 1 1
      src/components/molecules/DatetimePicker/index.jsx
  53. 4 3
      src/components/molecules/DatetimePicker/test.jsx
  54. 1 0
      src/components/molecules/DetailsNavigation/index.jsx
  55. 19 12
      src/components/molecules/DetailsNavigation/test.jsx
  56. 3 0
      src/components/molecules/Dropdown/index.jsx
  57. 25 35
      src/components/molecules/Dropdown/test.jsx
  58. 2 0
      src/components/molecules/DropdownFilter/index.jsx
  59. 36 0
      src/components/molecules/DropdownFilter/test.jsx
  60. 1 1
      src/components/molecules/DropdownLink/index.jsx
  61. 3 21
      src/components/molecules/DropdownLink/test.jsx
  62. 18 11
      src/components/molecules/EndpointField/index.jsx
  63. 61 47
      src/components/molecules/EndpointField/test.jsx
  64. 4 4
      src/components/molecules/EndpointListItem/index.jsx
  65. 27 19
      src/components/molecules/EndpointListItem/test.jsx
  66. 1 1
      src/components/molecules/LoadingButton/index.jsx
  67. 9 4
      src/components/molecules/LoadingButton/test.jsx
  68. 2 2
      src/components/molecules/LoginFormField/index.jsx
  69. 20 4
      src/components/molecules/LoginFormField/test.jsx
  70. 6 2
      src/components/molecules/LoginOptions/index.jsx
  71. 8 5
      src/components/molecules/LoginOptions/test.jsx
  72. 8 2
      src/components/molecules/MainListFilter/index.jsx
  73. 38 27
      src/components/molecules/MainListFilter/test.jsx
  74. 7 5
      src/components/molecules/MainListItem/index.jsx
  75. 27 19
      src/components/molecules/MainListItem/test.jsx
  76. 1 1
      src/components/molecules/Modal/index.jsx
  77. 16 11
      src/components/molecules/Modal/test.jsx
  78. 2 0
      src/components/molecules/NewItemDropdown/index.jsx
  79. 18 14
      src/components/molecules/NewItemDropdown/test.jsx
  80. 8 6
      src/components/molecules/NotificationDropdown/index.jsx
  81. 33 25
      src/components/molecules/NotificationDropdown/test.jsx
  82. 7 4
      src/components/molecules/PropertiesTable/index.jsx
  83. 38 22
      src/components/molecules/PropertiesTable/test.jsx
  84. 10 5
      src/components/molecules/ScheduleItem/index.jsx
  85. 54 0
      src/components/molecules/ScheduleItem/story.jsx
  86. 62 0
      src/components/molecules/ScheduleItem/test.jsx
  87. 4 3
      src/components/molecules/SearchInput/index.jsx
  88. 4 3
      src/components/molecules/SearchInput/test.jsx
  89. 3 2
      src/components/molecules/SideMenu/index.jsx
  90. 15 10
      src/components/molecules/SideMenu/test.jsx
  91. 4 3
      src/components/molecules/Table/index.jsx
  92. 21 11
      src/components/molecules/Table/test.jsx
  93. 2 2
      src/components/molecules/TaskItem/index.jsx
  94. 16 10
      src/components/molecules/TaskItem/test.jsx
  95. 4 1
      src/components/molecules/Timeline/index.jsx
  96. 31 22
      src/components/molecules/Timeline/test.jsx
  97. 3 2
      src/components/molecules/UserDropdown/index.jsx
  98. 26 19
      src/components/molecules/UserDropdown/test.jsx
  99. 2 2
      src/components/molecules/WizardBreadcrumbs/index.jsx
  100. 20 15
      src/components/molecules/WizardBreadcrumbs/test.jsx

+ 1 - 1
cypress.json

@@ -9,6 +9,6 @@
   "viewportWidth": 1280,
   "viewportHeight": 900,
   "defaultCommandTimeout": 7000,
-  "execTimeout": 90000,
+  "execTimeout": 5000,
   "requestTimeout": 20000
 }

+ 1 - 1
package.json

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

+ 4 - 4
private/cypress/integration/migration/1 - Create Openstack Endpoint.js

@@ -28,7 +28,7 @@ describe('Create Openstack Endpoint', () => {
   it('Shows new Openstack endpoint dialog', () => {
     cy.get('div').contains('New').click()
     cy.get('a').contains('Endpoint').click()
-    cy.get('div[data-test-id="endpointLogo-openstack"]').click()
+    cy.get('div[data-test-id="cProvider-endpointLogo-openstack"]').click()
   })
 
   it('Fills Openstack connection info', () => {
@@ -38,9 +38,9 @@ describe('Create Openstack Endpoint', () => {
     cy.get('input[placeholder="Password"]').type(config.endpoints.openstack.password)
     cy.get('input[placeholder="Auth URL"]').type(config.endpoints.openstack.authUrl)
     cy.get('input[placeholder="Project Name"]').type(config.endpoints.openstack.projectName)
-    cy.get('div[data-test-id="dropdown-glance_api_version"]').first().click()
+    cy.get('div[data-test-id="endpointField-dropdown-glance_api_version"]').first().click()
     cy.get('div[data-test-id="dropdownListItem"]').contains('2').click()
-    cy.get('div[data-test-id="dropdown-identity_api_version"]').first().click()
+    cy.get('div[data-test-id="endpointField-dropdown-identity_api_version"]').first().click()
     cy.get('div[data-test-id="dropdownListItem"]').contains('3').click()
     cy.get('input[placeholder="Project Domain Name"]').type(config.endpoints.openstack.projectDomainName)
     cy.get('input[placeholder="User Domain Name"]').type(config.endpoints.openstack.userDomainName)
@@ -54,6 +54,6 @@ describe('Create Openstack Endpoint', () => {
 
   it('Added Openstack to endpoint list', () => {
     cy.visit(`${config.nodeServer}endpoints/`)
-    cy.get('div[data-test-id="endpointListItemContent-e2e-openstack-test"]').should('contain', 'e2e-openstack-test')
+    cy.get('div[data-test-id="endpointListItem-content-e2e-openstack-test"]').should('contain', 'e2e-openstack-test')
   })
 })

+ 2 - 2
private/cypress/integration/migration/2 - Create VmWare Endpoint.js

@@ -28,7 +28,7 @@ describe('Create VmWare Endpoint', () => {
   it('Shows new VmWare endpoint dialog', () => {
     cy.get('div').contains('New').click()
     cy.get('a').contains('Endpoint').click()
-    cy.get('div[data-test-id="endpointLogo-vmware_vsphere"]').click()
+    cy.get('div[data-test-id="cProvider-endpointLogo-vmware_vsphere"]').click()
   })
 
   it('Fills VmWare connection info', () => {
@@ -46,6 +46,6 @@ describe('Create VmWare Endpoint', () => {
 
   it('Added Endpoint to endpoint list', () => {
     cy.visit(`${config.nodeServer}endpoints/`)
-    cy.get('div[data-test-id="endpointListItemContent-e2e-vmware-test"]').should('contain', 'e2e-vmware-test')
+    cy.get('div[data-test-id="endpointListItem-content-e2e-vmware-test"]').should('contain', 'e2e-vmware-test')
   })
 })

+ 5 - 5
private/cypress/integration/migration/3 - Create VmWare Openstack Migration.js

@@ -35,14 +35,14 @@ describe('Create VmWare to Openstack Migration', () => {
     cy.server()
     cy.route({ url: '**/instances**', method: 'GET' }).as('sourceInstances')
     cy.get('button').contains('Next').click()
-    cy.get('div[data-test-id="dropdown-vmware_vsphere"]').first().click()
+    cy.get('div[data-test-id="wEndpointList-dropdown-vmware_vsphere"]').first().click()
     cy.get('div').contains('e2e-vmware-test').click()
     cy.wait('@sourceInstances')
   })
 
   it('Chooses Openstack as Target Cloud', () => {
     cy.get('button').contains('Next').click()
-    cy.get('div[data-test-id="dropdown-openstack"]').first().click()
+    cy.get('div[data-test-id="wEndpointList-dropdown-openstack"]').first().click()
     cy.get('div').contains('e2e-openstack-test').click()
   })
 
@@ -52,9 +52,9 @@ describe('Create VmWare to Openstack Migration', () => {
     cy.route({ url: '**/instances**', method: 'GET' }).as('search')
     cy.get('input[placeholder="Search VMs"]').type(config.wizard.instancesSearch)
     cy.wait('@search')
-    cy.get('div[data-test-id="instanceItem"]').contains(config.wizard.instancesSearch)
-    cy.get('div[data-test-id="instanceItem"]').its('length').should('be.gt', 0)
-    cy.get('div[data-test-id="instanceItem"]').eq(config.wizard.instancesSelectItem).click()
+    cy.get('div[data-test-id="wInstances-instanceItem"]').contains(config.wizard.instancesSearch)
+    cy.get('div[data-test-id="wInstances-instanceItem"]').its('length').should('be.gt', 0)
+    cy.get('div[data-test-id="wInstances-instanceItem"]').eq(config.wizard.instancesSelectItem).click()
   })
 
   it('Fills Openstack migration info', () => {

+ 2 - 2
private/cypress/integration/migration/4 - Cancel first running migration.js

@@ -28,11 +28,11 @@ describe('Cancel a running migration', () => {
     cy.route({ url: '**/replicas/**', method: 'GET' }).as('replicas')
     cy.wait('@replicas')
     cy.get('a').contains('Migrations').click()
-    cy.get('div[data-test-id="statusPill-RUNNING"]').eq(0).click()
+    cy.get('div[data-test-id="mainListItem-statusPill-RUNNING"]').eq(0).click()
     cy.get('button').contains('Cancel').click()
     cy.route({ url: '**/actions', method: 'POST' }).as('cancel')
     cy.get('button').contains('Yes').click()
     cy.wait('@cancel')
-    cy.get('div[data-test-id="mainStatusPill-ERROR"]', { timeout: 120000 })
+    cy.get('div[data-test-id="dcHeader-statusPill-ERROR"]', { timeout: 120000 })
   })
 })

+ 1 - 1
private/cypress/integration/migration/6 - Delete first migration.js

@@ -28,7 +28,7 @@ describe('Delete the first migration', () => {
     cy.route({ url: '**/replicas/**', method: 'GET' }).as('replicas')
     cy.wait('@replicas')
     cy.get('a').contains('Migrations').click()
-    cy.get('div[data-test-id="mainListItem"]').first().click()
+    cy.get('div[data-test-id="mainListItem-content"]').first().click()
     cy.get('button').last().should('contain', 'Delete Migration').click()
     cy.route({ url: '**/migrations/**', method: 'DELETE' }).as('delete')
     cy.get('button').contains('Yes').click()

+ 2 - 2
private/cypress/integration/migration/7 - Delete e2e Openstack endpoint.js

@@ -32,8 +32,8 @@ describe('Delete the Openstack endpoint created for e2e testing', () => {
   })
 
   it('Delete e2e Openstack endpoint', () => {
-    cy.get('div[data-test-id="endpointListItemContent-e2e-openstack-test"]').should('contain', 'e2e-openstack-test')
-    cy.get('div[data-test-id="endpointListItemContent-e2e-openstack-test"]').first().click()
+    cy.get('div[data-test-id="endpointListItem-content-e2e-openstack-test"]').should('contain', 'e2e-openstack-test')
+    cy.get('div[data-test-id="endpointListItem-content-e2e-openstack-test"]').first().click()
     cy.server()
     cy.route({ url: '**/migrations/**', method: 'GET' }).as('migrations')
     cy.route({ url: '**/replicas/**', method: 'GET' }).as('replicas')

+ 2 - 2
private/cypress/integration/migration/8 - Delete e2e VmWare endpoint.js

@@ -32,8 +32,8 @@ describe('Delete the VmWare endpoint created for e2e testing', () => {
   })
 
   it('Delete e2e VmWare endpoint', () => {
-    cy.get('div[data-test-id="endpointListItemContent-e2e-vmware-test"]').should('contain', 'e2e-vmware-test')
-    cy.get('div[data-test-id="endpointListItemContent-e2e-vmware-test"]').first().click()
+    cy.get('div[data-test-id="endpointListItem-content-e2e-vmware-test"]').should('contain', 'e2e-vmware-test')
+    cy.get('div[data-test-id="endpointListItem-content-e2e-vmware-test"]').first().click()
     cy.server()
     cy.route({ url: '**/migrations/**', method: 'GET' }).as('migrations')
     cy.route({ url: '**/replicas/**', method: 'GET' }).as('replicas')

+ 3 - 3
private/cypress/integration/replica/1 - Create Azure Endpoint.js

@@ -28,12 +28,12 @@ describe('Create Azure Endpoint', () => {
   it('Shows new Azure endpoint dialog', () => {
     cy.get('div').contains('New').click()
     cy.get('a').contains('Endpoint').click()
-    cy.get('div[data-test-id="endpointLogo-azure"]').click()
+    cy.get('div[data-test-id="cProvider-endpointLogo-azure"]').click()
   })
 
   it('Fills Azure connection info', () => {
     cy.get('input[placeholder="Name"]').type('e2e-azure-test')
-    cy.get('div[data-test-id="switch-allow_untrusted"]').click()
+    cy.get('div[data-test-id="endpointField-switch-allow_untrusted"]').click()
     cy.get('input[placeholder="Username"]').type(config.endpoints.azure.username)
     cy.get('input[placeholder="Password"]').type(config.endpoints.azure.password)
     cy.get('input[placeholder="Subscription ID"]').type(config.endpoints.azure.subscriptionId)
@@ -47,6 +47,6 @@ describe('Create Azure Endpoint', () => {
 
   it('Added Endpoint to endpoint list', () => {
     cy.visit(`${config.nodeServer}endpoints/`)
-    cy.get('div[data-test-id="endpointListItemContent-e2e-azure-test"]').should('contain', 'e2e-azure-test')
+    cy.get('div[data-test-id="endpointListItem-content-e2e-azure-test"]').should('contain', 'e2e-azure-test')
   })
 })

+ 2 - 2
private/cypress/integration/replica/2 - Create VmWare Endpoint.js

@@ -28,7 +28,7 @@ describe('Create VmWare Endpoint', () => {
   it('Shows new VmWare endpoint dialog', () => {
     cy.get('div').contains('New').click()
     cy.get('a').contains('Endpoint').click()
-    cy.get('div[data-test-id="endpointLogo-vmware_vsphere"]').click()
+    cy.get('div[data-test-id="cProvider-endpointLogo-vmware_vsphere"]').click()
   })
 
   it('Fills VmWare connection info', () => {
@@ -46,6 +46,6 @@ describe('Create VmWare Endpoint', () => {
 
   it('Added Endpoint to endpoint list', () => {
     cy.visit(`${config.nodeServer}endpoints/`)
-    cy.get('div[data-test-id="endpointListItemContent-e2e-vmware-test"]').should('contain', 'e2e-vmware-test')
+    cy.get('div[data-test-id="endpointListItem-content-e2e-vmware-test"]').should('contain', 'e2e-vmware-test')
   })
 })

+ 8 - 8
private/cypress/integration/replica/3 - Create VmWare Azure Replica.js

@@ -35,14 +35,14 @@ describe('Create VmWare to Azure Replica', () => {
     cy.server()
     cy.route({ url: '**/instances**', method: 'GET' }).as('sourceInstances')
     cy.get('button').contains('Next').click()
-    cy.get('div[data-test-id="dropdown-vmware_vsphere"]').first().click()
+    cy.get('div[data-test-id="wEndpointList-dropdown-vmware_vsphere"]').first().click()
     cy.get('div').contains('e2e-vmware-test').click()
     cy.wait('@sourceInstances')
   })
 
   it('Chooses Azure as Target Cloud', () => {
     cy.get('button').contains('Next').click()
-    cy.get('div[data-test-id="dropdown-azure"]').first().click()
+    cy.get('div[data-test-id="wEndpointList-dropdown-azure"]').first().click()
     cy.get('div').contains('e2e-azure-test').click()
   })
 
@@ -52,9 +52,9 @@ describe('Create VmWare to Azure Replica', () => {
     cy.route({ url: '**/instances**', method: 'GET' }).as('search')
     cy.get('input[placeholder="Search VMs"]').type(config.wizard.instancesSearch)
     cy.wait('@search')
-    cy.get('div[data-test-id="instanceItem"]').contains(config.wizard.instancesSearch)
-    cy.get('div[data-test-id="instanceItem"]').its('length').should('be.gt', 0)
-    cy.get('div[data-test-id="instanceItem"]').eq(config.wizard.instancesSelectItem).click()
+    cy.get('div[data-test-id="wInstances-instanceItem"]').contains(config.wizard.instancesSearch)
+    cy.get('div[data-test-id="wInstances-instanceItem"]').its('length').should('be.gt', 0)
+    cy.get('div[data-test-id="wInstances-instanceItem"]').eq(config.wizard.instancesSelectItem).click()
   })
 
   it('Fills Azure replica info', () => {
@@ -62,9 +62,9 @@ describe('Create VmWare to Azure Replica', () => {
     cy.get('input[placeholder="Location"]').type(config.wizard.azure.location.value)
     cy.get('input[placeholder="Resource Group"]').type(config.wizard.azure.resourceGroup.value)
 
-    // cy.get('div[data-test-id="dropdown-location"]').first().click()
-    // cy.get('div[data-test-id="dropdownListItem"]').contains(config.wizard.azure.location.label).click()
-    // cy.get('div[data-test-id="dropdown-resource_group"]').first().click()
+    // cy.get('div[data-test-id="wOptionsField-dropdown-location"]').first().click()
+    // cy.get('div[data-test-id="wOptionsField-dropdownListItem"]').contains(config.wizard.azure.location.label).click()
+    // cy.get('div[data-test-id="wOptionsField-dropdown-resource_group"]').first().click()
     // cy.get('div[data-test-id="dropdownListItem"]').contains(config.wizard.azure.resourceGroup.label).click()
   })
 

+ 2 - 2
private/cypress/integration/replica/4 - Cancel first running replica.js

@@ -26,13 +26,13 @@ describe('Cancel a running replica', () => {
   it('Cancels replica execution', () => {
     cy.server()
     cy.route({ url: '**/executions/detail', method: 'GET' }).as('execution')
-    cy.get('div[data-test-id="statusPill-RUNNING"]').eq(0).click()
+    cy.get('div[data-test-id="mainListItem-statusPill-RUNNING"]').eq(0).click()
     cy.wait('@execution')
     cy.get('a').contains('Executions').click()
     cy.get('button').contains('Cancel Execution').click()
     cy.route({ url: '**/actions', method: 'POST' }).as('cancel')
     cy.get('button').contains('Yes').click()
     cy.wait('@cancel')
-    cy.get('div[data-test-id="mainStatusPill-ERROR"]', { timeout: 120000 })
+    cy.get('div[data-test-id="dcHeader-statusPill-ERROR"]', { timeout: 120000 })
   })
 })

+ 1 - 1
private/cypress/integration/replica/5 - Cannot delete used endpoint.js

@@ -25,7 +25,7 @@ describe('Cannot delete used endpoint', () => {
 
   it('Should show in usage message when trying to delete', () => {
     cy.get('div').contains('Cloud Endpoints').first().click()
-    cy.get('div[data-test-id="endpointListItemContent-e2e-azure-test"]').click()
+    cy.get('div[data-test-id="endpointListItem-content-e2e-azure-test"]').first().click()
     cy.get('button').contains('Delete Endpoint').click()
     cy.get('div[data-test-id="alertModal"]').should('contain', 'The endpoint can\'t be deleted because it is in use by replicas or migrations.')
   })

+ 1 - 1
private/cypress/integration/replica/6 - Delete first replica.js

@@ -26,7 +26,7 @@ describe('Delete the first replica', () => {
   it('Delete replica', () => {
     cy.server()
     cy.route({ url: '**/executions/**', method: 'GET' }).as('executions')
-    cy.get('div[data-test-id="mainListItem"]').first().click()
+    cy.get('div[data-test-id="mainListItem-content"]').first().click()
     cy.wait('@executions')
     cy.get('button').last().should('contain', 'Delete Replica').click()
     cy.route({ url: '**/replicas/**', method: 'DELETE' }).as('delete')

+ 2 - 2
private/cypress/integration/replica/7 - Delete e2e Azure endpoint.js

@@ -32,8 +32,8 @@ describe('Delete the Azure endpoint created for e2e testing', () => {
   })
 
   it('Delete e2e Azure endpoint', () => {
-    cy.get('div[data-test-id="endpointListItemContent-e2e-azure-test"]').should('contain', 'e2e-azure-test')
-    cy.get('div[data-test-id="endpointListItemContent-e2e-azure-test"]').first().click()
+    cy.get('div[data-test-id="endpointListItem-content-e2e-azure-test"]').should('contain', 'e2e-azure-test')
+    cy.get('div[data-test-id="endpointListItem-content-e2e-azure-test"]').first().click()
     cy.server()
     cy.route({ url: '**/migrations/**', method: 'GET' }).as('migrations')
     cy.route({ url: '**/replicas/**', method: 'GET' }).as('replicas')

+ 2 - 2
private/cypress/integration/replica/8 - Delete e2e VmWare endpoint.js

@@ -32,8 +32,8 @@ describe('Delete the VmWare endpoint created for e2e testing', () => {
   })
 
   it('Delete e2e VmWare endpoint', () => {
-    cy.get('div[data-test-id="endpointListItemContent-e2e-vmware-test"]').should('contain', 'e2e-vmware-test')
-    cy.get('div[data-test-id="endpointListItemContent-e2e-vmware-test"]').first().click()
+    cy.get('div[data-test-id="endpointListItem-content-e2e-vmware-test"]').should('contain', 'e2e-vmware-test')
+    cy.get('div[data-test-id="endpointListItem-content-e2e-vmware-test"]').first().click()
     cy.server()
     cy.route({ url: '**/migrations/**', method: 'GET' }).as('migrations')
     cy.route({ url: '**/replicas/**', method: 'GET' }).as('replicas')

+ 11 - 11
private/cypress/integration/scheduler/Scheduler Operations.js

@@ -26,7 +26,7 @@ describe('Scheduler Operations', () => {
   it('Goes to scheduler\'s page', () => {
     cy.server()
     cy.route('GET', '**/executions/detail').as('execution')
-    cy.get('div[data-test-id="mainListItem"]').first().click()
+    cy.get('div[data-test-id="mainListItem-content"]').first().click()
     cy.wait('@execution')
     cy.get('a').contains('Schedule').click()
     cy.get('button').should('contain', 'Add Schedule')
@@ -37,20 +37,20 @@ describe('Scheduler Operations', () => {
     cy.route('POST', '**/schedules').as('schedule')
     cy.get('button').contains('Add Schedule').click()
     cy.wait('@schedule')
-    cy.get('div[data-test-id="saveButton"]').should('not.be.visible')
+    cy.get('div[data-test-id="scheduleItem-saveButton"]').should('not.be.visible')
   })
 
   it('Changes the month', () => {
-    cy.get('div[data-test-id="monthDropdown"]').last().click()
+    cy.get('div[data-test-id="scheduleItem-monthDropdown"]').last().click()
     cy.get('div[data-test-id="dropdownListItem"]').contains('October').click()
-    cy.get('div[data-test-id="monthDropdown"]').last().should('contain', 'October')
-    cy.get('div[data-test-id="saveButton"]').should('be.visible')
+    cy.get('div[data-test-id="scheduleItem-monthDropdown"]').last().should('contain', 'October')
+    cy.get('div[data-test-id="scheduleItem-saveButton"]').should('be.visible')
   })
 
   it('Changes the hour', () => {
-    cy.get('div[data-test-id="hourDropdown"]').last().click()
+    cy.get('div[data-test-id="scheduleItem-hourDropdown"]').last().click()
     cy.get('div[data-test-id="dropdownListItem"]').contains('04').click()
-    cy.get('div[data-test-id="hourDropdown"]').last().should('contain', '04')
+    cy.get('div[data-test-id="scheduleItem-hourDropdown"]').last().should('contain', '04')
   })
 
   it('Changes timezone', () => {
@@ -61,19 +61,19 @@ describe('Scheduler Operations', () => {
       utcTime = `0${utcTime}`
     }
     utcTime = utcTime.toString()
-    cy.get('div[data-test-id="hourDropdown"]').last().should('contain', utcTime)
+    cy.get('div[data-test-id="scheduleItem-hourDropdown"]').last().should('contain', utcTime)
   })
 
   it('Saves the changes', () => {
     cy.server()
     cy.route('PUT', '**/schedules/**').as('schedule')
-    cy.get('div[data-test-id="saveButton"]').should('be.visible').last().click()
+    cy.get('div[data-test-id="scheduleItem-saveButton"]').should('be.visible').last().click()
     cy.wait('@schedule')
-    cy.get('div[data-test-id="saveButton"]').should('not.be.visible')
+    cy.get('div[data-test-id="scheduleItem-saveButton"]').should('not.be.visible')
   })
 
   it('Deletes the last schedule', () => {
-    cy.get('div[data-test-id="deleteButton"]').last().click()
+    cy.get('div[data-test-id="scheduleItem-deleteButton"]').last().click()
     cy.server()
     cy.route('DELETE', '**/schedules/**').as('schedule')
     cy.get('button').contains('Yes').click()

+ 2 - 0
src/components/atoms/Checkbox/index.jsx

@@ -55,6 +55,7 @@ type Props = {
   checked?: boolean,
   disabled?: boolean,
   onChange?: (checked: boolean) => void,
+  'data-test-id'?: string,
 }
 @observer
 class Checkbox extends React.Component<Props> {
@@ -69,6 +70,7 @@ class Checkbox extends React.Component<Props> {
   render() {
     return (
       <Wrapper
+        data-test-id={this.props['data-test-id'] || 'checkbox'}
         className={this.props.className}
         onClick={() => { this.handleClick() }}
         checked={this.props.checked}

+ 1 - 1
src/components/atoms/CopyValue/index.jsx

@@ -70,7 +70,7 @@ class CopyValue extends React.Component<Props> {
         data-test-id={this.props['data-test-id'] || 'copyValue'}
       >
         <Value
-          data-test-id="value"
+          data-test-id="copyValue-value"
           width={this.props.width}
           maxWidth={this.props.maxWidth}
         >{this.props.value}</Value>

+ 3 - 2
src/components/atoms/CopyValue/test.jsx

@@ -17,14 +17,15 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import React from 'react'
 import { shallow } from 'enzyme'
 import sinon from 'sinon'
+import TestWrapper from '../../../utils/TestWrapper'
 import CopyValue from '.'
 
-const wrap = props => shallow(<CopyValue value="the_value" {...props} />)
+const wrap = props => new TestWrapper(shallow(<CopyValue value="the_value" {...props} />), 'copyValue')
 
 describe('CopyValue Component', () => {
   it('renders `value`', () => {
     const wrapper = wrap()
-    expect(wrapper.findWhere(w => w.prop('data-test-id') === 'value').dive().text()).toBe('the_value')
+    expect(wrapper.findText('value')).toBe('the_value')
   })
 
   it('copies `value` to clipboard', () => {

+ 1 - 1
src/components/atoms/DropdownButton/index.jsx

@@ -142,7 +142,7 @@ class DropdownButton extends React.Component<Props> {
           {...this.props}
           onClick={() => {}}
           innerRef={() => {}}
-          data-test-id="dropdownButtonValue"
+          data-test-id="dropdownButton-value"
           disabled={this.props.disabled}
         >{this.props.value}</Label>
         <Arrow

+ 3 - 2
src/components/atoms/DropdownButton/test.jsx

@@ -17,14 +17,15 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import React from 'react'
 import { shallow } from 'enzyme'
 import sinon from 'sinon'
+import TestWrapper from '../../../utils/TestWrapper'
 import DropdownButton from '.'
 
-const wrap = props => shallow(<DropdownButton {...props} />)
+const wrap = props => new TestWrapper(shallow(<DropdownButton {...props} />), 'dropdownButton')
 
 describe('DropdownButton Component', () => {
   it('renders the given value', () => {
     const wrapper = wrap({ value: 'the_value' })
-    expect(wrapper.findWhere(w => w.prop('data-test-id') === 'dropdownButtonValue').dive().text()).toBe('the_value')
+    expect(wrapper.findText('value')).toBe('the_value')
   })
 
   it('calls click handler', () => {

+ 10 - 2
src/components/atoms/EndpointLogos/index.jsx

@@ -119,6 +119,7 @@ type Props = {
   endpoint?: string,
   height: number,
   disabled?: boolean,
+  'data-test-id'?: string,
 }
 @observer
 class EndpointLogos extends React.Component<Props> {
@@ -149,7 +150,14 @@ class EndpointLogos extends React.Component<Props> {
   }
 
   renderGenericLogo(size: {w: number, h: number}) {
-    return <Generic size={size} name={this.props.endpoint || ''} disabled={this.props.disabled} />
+    return (
+      <Generic
+        data-test-id="endpointLogos-genericLogo"
+        size={size}
+        name={this.props.endpoint || ''}
+        disabled={this.props.disabled}
+      />
+    )
   }
 
   render() {
@@ -166,7 +174,7 @@ class EndpointLogos extends React.Component<Props> {
     }
 
     return (
-      <Wrapper {...this.props}>
+      <Wrapper {...this.props} data-test-id={this.props['data-test-id'] || 'endpointLogos'}>
         <Logo
           width={size.w}
           height={size.h}

+ 20 - 11
src/components/atoms/EndpointLogos/test.jsx

@@ -16,23 +16,32 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import React from 'react'
 import { shallow } from 'enzyme'
+import TestWrapper from '../../../utils/TestWrapper'
 import EndpointLogos from '.'
 
-const wrap = props => shallow(<EndpointLogos {...props} />)
+const wrap = props => new TestWrapper(shallow(<EndpointLogos {...props} />), 'endpointLogos')
 
 describe('EndpointLogos Component', () => {
   it('renders 32px aws', () => {
     const wrapper = wrap({ height: 32, endpoint: 'aws' })
-    const logo = wrapper.findWhere(w => w.prop('data-test-id') === 'endpointLogos-logo')
-    expect(logo.prop('height')).toBe(32)
-    console.log(logo.prop('imageInfo'))
+    const logo = wrapper.find('logo')
+    expect(logo.prop('imageInfo').h).toBe(32)
+    expect(logo.prop('imageInfo').image).toBe('file')
+    expect(wrapper.prop('endpoint')).toBe('aws')
   })
 
-  // it('passes down props', () => {
-  //   let wrapper = wrap({ height: 32, endpoint: 'aws' })
-  //   expect(wrapper.prop('height')).toBe(32)
-  //   let imageInfo = wrapper.findWhere(w => w.name() === 'styled.div' && w.prop('imageInfo')).prop('imageInfo')
-  //   expect(imageInfo.h).toBe(32)
-  //   expect(imageInfo.image).toBe('file')
-  // })
+  it('renders 128px azure disabled', () => {
+    const wrapper = wrap({ height: 128, endpoint: 'azure', disabled: true })
+    const logo = wrapper.find('logo')
+    expect(logo.prop('imageInfo').h).toBe(128)
+    expect(logo.prop('imageInfo').disabled).toBe(true)
+    expect(wrapper.prop('endpoint')).toBe('azure')
+  })
+
+  it('renders 64px generic logo', () => {
+    const wrapper = wrap({ height: 64, endpoint: 'generic' })
+    const logo = wrapper.find('genericLogo')
+    expect(logo.prop('name')).toBe('generic')
+    expect(logo.prop('size').h).toBe(64)
+  })
 })

+ 25 - 0
src/components/atoms/InfoIcon/story.jsx

@@ -0,0 +1,25 @@
+/*
+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/>.
+*/
+
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import InfoIcon from '.'
+
+storiesOf('InfoIcon', module)
+  .add('default', () => (
+    <InfoIcon />
+  ))
+  .add('warning', () => (
+    <InfoIcon warning />
+  ))

+ 1 - 1
src/components/atoms/PasswordValue/index.jsx

@@ -69,7 +69,7 @@ class PasswordValue extends React.Component<Props, State> {
   render() {
     return (
       <Wrapper onClick={() => { this.handleShowClick() }} show={this.state.show}>
-        <Value>{this.state.show ? this.props.value : '•••••••••'}</Value>
+        <Value data-test-id="passwordValue-value">{this.state.show ? this.props.value : '•••••••••'}</Value>
         {!this.state.show ? <EyeIcon /> : null}
       </Wrapper>
     )

+ 15 - 11
src/components/atoms/PasswordValue/test.jsx

@@ -12,20 +12,24 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import PasswordValue from '.'
 
-const wrap = props => shallow(<PasswordValue {...props} />)
-const text = html => html.substring(html.indexOf('>') + 1, html.lastIndexOf('<'))
-it('conceals the password', () => {
-  let password = text(wrap({ value: 'test' }).children().first().html())
-  expect(password).toBe('•••••••••')
-})
+const wrap = props => new TestWrapper(shallow(<PasswordValue value="the_value" {...props} />), 'passwordValue')
+
+describe('PasswordValue Component', () => {
+  it('conceals the password', () => {
+    const wrapper = wrap()
+    expect(wrapper.findText('value')).toBe('•••••••••')
+  })
 
-it('reveals password on click', () => {
-  let wrapper = wrap({ value: 'test' })
-  wrapper.simulate('click')
-  let password = text(wrapper.children().first().html())
-  expect(password).toBe('test')
+  it('reveals password on click', () => {
+    const wrapper = wrap()
+    wrapper.simulate('click')
+    expect(wrapper.findText('value')).toBe('the_value')
+  })
 })

+ 1 - 1
src/components/atoms/ProgressBar/index.jsx

@@ -41,7 +41,7 @@ class ProgressBar extends React.Component<Props> {
   render() {
     return (
       <Wrapper {...this.props}>
-        <Progress width={this.props.progress} />
+        <Progress data-test-id="progressBar-progress" width={this.props.progress} />
       </Wrapper>
     )
   }

+ 9 - 4
src/components/atoms/ProgressBar/test.jsx

@@ -12,13 +12,18 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import ProgressBar from '.'
 
-const wrap = props => shallow(<ProgressBar {...props} />).dive()
+const wrap = props => new TestWrapper(shallow(<ProgressBar {...props} />), 'progressBar')
 
-it('has width according to progress number', () => {
-  let progressDiv = wrap({ progress: 61 }).children()
-  expect(progressDiv.prop('width')).toBe(61)
+describe('ProgressBar Component', () => {
+  it('has width according to progress number', () => {
+    const wrapper = wrap({ progress: 61 })
+    expect(wrapper.find('progress').prop('width')).toBe(61)
+  })
 })

+ 2 - 2
src/components/atoms/RadioInput/index.jsx

@@ -60,8 +60,8 @@ class RadioInput extends React.Component<Props> {
     return (
       <Wrapper {...this.props}>
         <LabelStyled>
-          <InputStyled type="radio" {...this.props} />
-          <Text>{this.props.label}</Text>
+          <InputStyled type="radio" {...this.props} data-test-id="radioInput-input" />
+          <Text data-test-id="radioInput-label">{this.props.label}</Text>
         </LabelStyled>
       </Wrapper>
     )

+ 9 - 5
src/components/atoms/RadioInput/test.jsx

@@ -12,14 +12,18 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import RadioInput from '.'
 
-const wrap = props => shallow(<RadioInput {...props} />)
+const wrap = props => new TestWrapper(shallow(<RadioInput {...props} />), 'radioInput')
 
-it('renders the given label', () => {
-  let wrapper = wrap({ label: 'test' })
-  let doc = new DOMParser().parseFromString(wrapper.html(), 'text/xml')
-  expect(doc.firstChild.querySelector('div').innerHTML).toBe('test')
+describe('RadioInput Component', () => {
+  it('renders the given label', () => {
+    const wrapper = wrap({ label: 'the_value' })
+    expect(wrapper.findText('label')).toBe('the_value')
+  })
 })

+ 7 - 1
src/components/atoms/ReloadButton/index.jsx

@@ -36,6 +36,7 @@ injectGlobal`
 
 type Props = {
   onClick: () => void,
+  'data-test-id'?: string
 }
 @observer
 class ReloadButton extends React.Component<Props> {
@@ -64,7 +65,12 @@ class ReloadButton extends React.Component<Props> {
 
   render() {
     return (
-      <Wrapper innerRef={div => { this.wrapper = div }} {...this.props} onClick={() => { this.onClick() }} />
+      <Wrapper
+        data-test-id={this.props['data-test-id'] || 'reloadButton'}
+        innerRef={div => { this.wrapper = div }}
+        {...this.props}
+        onClick={() => { this.onClick() }}
+      />
     )
   }
 }

+ 9 - 5
src/components/atoms/ReloadButton/test.jsx

@@ -12,6 +12,8 @@ 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'
@@ -19,9 +21,11 @@ import ReloadButton from '.'
 
 const wrap = props => shallow(<ReloadButton {...props} />)
 
-it('handles click', () => {
-  let onClick = sinon.spy()
-  let wrapper = wrap({ onClick })
-  wrapper.simulate('click')
-  expect(onClick.calledOnce).toBe(true)
+describe('ReloadButton Component', () => {
+  it('handles click', () => {
+    let onClick = sinon.spy()
+    let wrapper = wrap({ onClick })
+    wrapper.simulate('click')
+    expect(onClick.calledOnce).toBe(true)
+  })
 })

+ 1 - 1
src/components/atoms/SearchButton/images/filter.js

@@ -1,6 +1,6 @@
 const filter = color => `
 <?xml version="1.0" encoding="UTF-8"?>
-<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<svg data-test-id="searchButton-filterIcon" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
     <title>Search</title>
     <desc>Created with Sketch.</desc>

+ 1 - 1
src/components/atoms/SearchButton/images/search.js

@@ -13,7 +13,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 const search = color => `<?xml version="1.0" encoding="UTF-8"?>
-<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" 
+<svg data-test-id="searchButton-searchIcon" width="14px" height="14px" viewBox="0 0 14 14" version="1.1" 
 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <!-- Generator: Sketch 47 (45396) - http://www.bohemiancoding.com/sketch -->
     <title>Search</title>

+ 10 - 8
src/components/atoms/SearchButton/index.jsx

@@ -36,20 +36,22 @@ const Icon = styled.div`
 `
 
 type Props = {
-  className: string,
-  primary: boolean,
-  useFilterIcon: boolean,
+  className?: string,
+  primary?: boolean,
+  useFilterIcon?: boolean,
 }
 @observer
 class SearchButton extends React.Component<Props> {
   render() {
     return (
       <Wrapper className={this.props.className} {...this.props}>
-        <Icon dangerouslySetInnerHTML={{
-          __html: this.props.useFilterIcon ?
-            filterImage(Palette.grayscale[3]) :
-            searchImage(this.props.primary ? Palette.primary : Palette.grayscale[4]),
-        }}
+        <Icon
+          data-test-id="searchButton-icon"
+          dangerouslySetInnerHTML={{
+            __html: this.props.useFilterIcon ?
+              filterImage(Palette.grayscale[3]) :
+              searchImage(this.props.primary ? Palette.primary : Palette.grayscale[4]),
+          }}
         />
       </Wrapper>
     )

+ 8 - 0
src/components/atoms/SearchButton/story.jsx

@@ -12,6 +12,8 @@ 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 SearchButton from '.'
@@ -20,3 +22,9 @@ storiesOf('SearchButton', module)
   .add('default', () => (
     <SearchButton />
   ))
+  .add('primary', () => (
+    <SearchButton primary />
+  ))
+  .add('filter icon', () => (
+    <SearchButton useFilterIcon />
+  ))

+ 24 - 6
src/components/atoms/SearchButton/test.jsx

@@ -12,16 +12,34 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import SearchButton from '.'
 
-const wrap = props => shallow(<SearchButton {...props} />)
+const wrap = props => new TestWrapper(shallow(<SearchButton {...props} />), 'searchButton')
+
+describe('SearchButton Component', () => {
+  it('uses filter or search icon', () => {
+    const getIconId = (w: TestWrapper): string => {
+      /* eslint no-underscore-dangle: off */
+      const iconSvg = w.find('icon').prop('dangerouslySetInnerHTML').__html
+      return /data-test-id="(.*?)"/g.exec(iconSvg)[1]
+    }
+    let wrapper = wrap()
+    expect(getIconId(wrapper)).toBe('searchButton-searchIcon')
+
+    wrapper = wrap({ useFilterIcon: true })
+    expect(getIconId(wrapper)).toBe('searchButton-filterIcon')
+  })
 
-it('handles click', () => {
-  let onClick = sinon.spy()
-  let wrapper = wrap({ onClick })
-  wrapper.simulate('click')
-  expect(onClick.calledOnce).toBe(true)
+  it('handles click', () => {
+    let onClick = sinon.spy()
+    let wrapper = wrap({ onClick })
+    wrapper.simulate('click')
+    expect(onClick.calledOnce).toBe(true)
+  })
 })

+ 6 - 2
src/components/atoms/StatusIcon/test.jsx

@@ -12,12 +12,16 @@ 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 StatusIcon from '.'
 
 const wrap = props => shallow(<StatusIcon {...props} />)
 
-it('renders with props', () => {
-  expect(wrap({ status: 'success' }).prop('status')).toBe('success')
+describe('StatusIcon Component', () => {
+  it('renders with props', () => {
+    expect(wrap({ status: 'success' }).prop('status')).toBe('success')
+  })
 })

+ 6 - 1
src/components/atoms/StatusImage/index.jsx

@@ -96,6 +96,7 @@ class StatusImage extends React.Component<Props> {
             stroke={Palette.grayscale[2]}
           />
           <CircleProgressBar
+            data-test-id="statusImage-progressBar"
             r="47"
             cx="48"
             cy="48"
@@ -114,7 +115,11 @@ class StatusImage extends React.Component<Props> {
       return null
     }
 
-    return <ProgressText>{this.props.loadingProgress ? this.props.loadingProgress.toFixed(0) : 0}%</ProgressText>
+    return (
+      <ProgressText
+        data-test-id="statusImage-progressText"
+      >{this.props.loadingProgress ? this.props.loadingProgress.toFixed(0) : 0}%</ProgressText>
+    )
   }
 
   render() {

+ 14 - 3
src/components/atoms/StatusImage/test.jsx

@@ -12,12 +12,23 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import StatusImage from '.'
 
-const wrap = props => shallow(<StatusImage {...props} />)
+const wrap = props => new TestWrapper(shallow(<StatusImage {...props} />), 'statusImage')
+
+describe('StatusImage Component', () => {
+  it('renders with status \'SUCCESS\' prop', () => {
+    expect(wrap({ status: 'success' }).prop('status')).toBe('success')
+  })
 
-it('renders with props', () => {
-  expect(wrap({ status: 'success' }).prop('status')).toBe('success')
+  it('renders progress', () => {
+    const wrapper = wrap({ loading: true, loadingProgress: 45 })
+    expect(wrapper.find('progressBar').prop('strokeDashoffset')).toBe(165)
+    expect(wrapper.findText('progressText')).toBe('45%')
+  })
 })

+ 11 - 9
src/components/atoms/StatusPill/test.jsx

@@ -12,20 +12,22 @@ 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 StatusPill from '.'
 
 const wrap = props => shallow(<StatusPill {...props} />)
 
-it('renders label if given', () => {
-  let wrapper = wrap({ label: 'test', status: 'COMPLETED' })
-  let label = new DOMParser().parseFromString(wrapper.html(), 'text/xml').firstChild.innerHTML
-  expect(label).toBe('test')
-})
+describe('StatusPill Component', () => {
+  it('renders label if given', () => {
+    let wrapper = wrap({ label: 'the_value', status: 'COMPLETED' })
+    expect(wrapper.dive().text()).toBe('the_value')
+  })
 
-it('renders status as label if no label is given', () => {
-  let wrapper = wrap({ status: 'COMPLETED' })
-  let label = new DOMParser().parseFromString(wrapper.html(), 'text/xml').firstChild.innerHTML
-  expect(label).toBe('COMPLETED')
+  it('renders status as label if no label is given', () => {
+    let wrapper = wrap({ status: 'COMPLETED' })
+    expect(wrapper.dive().text()).toBe('COMPLETED')
+  })
 })

+ 2 - 2
src/components/atoms/Switch/index.jsx

@@ -126,7 +126,7 @@ type Props = {
   big: boolean,
   checkedLabel: string,
   uncheckedLabel: string,
-  dataTestId?: string,
+  'data-test-id'?: string,
   style: {[string]: mixed},
 }
 type State = {
@@ -183,7 +183,7 @@ class Switch extends React.Component<Props, State> {
         secondary={this.props.secondary}
         disabled={this.props.disabled}
         onClick={() => { this.handleInputChange() }}
-        data-test-id={this.props.dataTestId}
+        data-test-id={this.props['data-test-id'] || 'switch-input'}
       >
         <InputBackground
           triState={this.props.triState}

+ 24 - 20
src/components/atoms/Switch/test.jsx

@@ -12,32 +12,36 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import Switch from '.'
 
-const wrap = props => shallow(<Switch {...props} />)
+const wrap = props => new TestWrapper(shallow(<Switch {...props} />), 'switch')
 
-it('handles change event', () => {
-  let onChange = sinon.spy()
-  let wrapper = wrap({ onChange, checked: false })
-  wrapper.children().first().simulate('click')
-  expect(onChange.args[0][0]).toBe(true)
-})
+describe('Switch Component', () => {
+  it('handles change event', () => {
+    let onChange = sinon.spy()
+    let wrapper = wrap({ onChange, checked: false })
+    wrapper.find('input').simulate('click')
+    expect(onChange.args[0][0]).toBe(true)
+  })
 
-it('dispatches `null` if in tri-state `false` and clicked', () => {
-  let onChange = sinon.spy()
-  let wrapper = wrap({ onChange, checked: false, triState: true })
-  wrapper.children().first().simulate('click')
-  expect(onChange.args[0][0]).toBe(null)
-})
+  it('dispatches `null` if in tri-state `false` and clicked', () => {
+    let onChange = sinon.spy()
+    let wrapper = wrap({ onChange, checked: false, triState: true })
+    wrapper.find('input').simulate('click')
+    expect(onChange.args[0][0]).toBe(null)
+  })
 
-it('dispatches `true` if in tri-state `null` after `false` and clicked', () => {
-  let onChange = sinon.spy()
-  let wrapper = wrap({ onChange, checked: false, triState: true })
-  let input = wrapper.children().first()
-  input.simulate('click')
-  input.simulate('click')
-  expect(onChange.args[0][0]).toBe(null)
+  it('dispatches `true` if in tri-state `null` after `false` and clicked', () => {
+    let onChange = sinon.spy()
+    let wrapper = wrap({ onChange, checked: false, triState: true })
+    wrapper.find('input').simulate('click')
+    wrapper.find('input').simulate('click')
+    expect(onChange.args[0][0]).toBe(null)
+  })
 })

+ 22 - 0
src/components/atoms/TextArea/story.jsx

@@ -0,0 +1,22 @@
+/*
+Copyright (C) 2018  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 TextArea from '.'
+
+storiesOf('TextArea', module)
+  .add('default', () => <TextArea />)

+ 32 - 0
src/components/atoms/TextArea/test.jsx

@@ -0,0 +1,32 @@
+/*
+Copyright (C) 2018  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 TextArea from '.'
+
+const wrap = props => shallow(<TextArea {...props} />)
+
+describe('TextArea Component', () => {
+  it('dispatches change', () => {
+    const onChange = sinon.spy()
+    const wrapper = wrap({ value: 'the_value', onChange })
+    expect(wrapper.prop('value')).toBe('the_value')
+    wrapper.simulate('change', { value: 'A' })
+    expect(onChange.args[0][0].value).toBe('A')
+  })
+})

+ 6 - 3
src/components/atoms/TextInput/index.jsx

@@ -98,23 +98,26 @@ type Props = {
   showClose?: boolean,
   onCloseClick?: () => void,
   embedded?: boolean,
+  'data-test-id'?: string,
 }
 const TextInput = (props: Props) => {
   const { _ref, required, value, onChange, showClose, onCloseClick } = props
   let input
   return (
-    <Wrapper>
+    <Wrapper data-test-id={props['data-test-id'] || 'textInput'}>
       <Input
         innerRef={ref => { input = ref; if (_ref) _ref(ref) }}
         type="text"
         customRequired={required}
         value={value}
         onChange={onChange}
+        data-test-id="textInput-input"
         {...props}
       />
-      <Required show={required} />
+      <Required show={required} data-test-id="textInput-required" />
       <Close
-        show={showClose && value}
+        data-test-id="textInput-close"
+        show={showClose && value !== '' && value !== undefined}
         onClick={() => {
           input.focus()
           // $FlowIgnore

+ 38 - 5
src/components/atoms/TextInput/test.jsx

@@ -12,14 +12,47 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import TextInput from '.'
 
-const wrap = props => shallow(<TextInput {...props} />)
+const wrap = props => new TestWrapper(shallow(<TextInput {...props} />), 'textInput')
+
+describe('TextInput Component', () => {
+  it('dispatches change', () => {
+    const onChange = sinon.spy()
+    const wrapper = wrap({ value: 'the_value', onChange })
+    const input = wrapper.find('input')
+    expect(input.prop('value')).toBe('the_value')
+    input.simulate('change', { value: 'A' })
+    expect(onChange.args[0][0].value).toBe('A')
+  })
+
+  it('shows required icon', () => {
+    let wrapper = wrap()
+    let required = wrapper.find('required')
+    expect(required.prop('show')).toBe(undefined)
+    wrapper = wrap({ required: true })
+    required = wrapper.find('required')
+    expect(required.prop('show')).toBe(true)
+  })
 
-it('passes props', () => {
-  let onChange = () => { }
-  let input = wrap({ onChange }).children().first()
-  expect(input.prop('onChange')).toBe(onChange)
+  it('shows close icon', () => {
+    let wrapper = wrap()
+    let close = wrapper.find('close')
+    expect(close.prop('show')).toBe(undefined)
+    wrapper = wrap({ showClose: true, value: 'the_value' })
+    close = wrapper.find('close')
+    expect(close.prop('show')).toBe(true)
+    wrapper = wrap({ showClose: true, value: '' })
+    close = wrapper.find('close')
+    expect(close.prop('show')).toBe(false)
+    wrapper = wrap({ showClose: true })
+    close = wrapper.find('close')
+    expect(close.prop('show')).toBe(false)
+  })
 })

+ 4 - 3
src/components/atoms/ToggleButtonBar/index.jsx

@@ -54,8 +54,8 @@ const Item = styled.div`
 type ItemType = { value: string, label: string }
 type Props = {
   items: Array<ItemType>,
-  selectedValue: string,
-  onChange: (item: ItemType) => void,
+  selectedValue?: string,
+  onChange?: (item: ItemType) => void,
   className?: string,
 }
 @observer
@@ -70,9 +70,10 @@ class ToggleButtonBar extends React.Component<Props> {
         {this.props.items.map(item => {
           return (
             <Item
+              data-test-id={`toggleButtonBar-${item.value}`}
               key={item.value}
               selected={this.props.selectedValue === item.value}
-              onClick={() => { this.props.onChange(item) }}
+              onClick={() => { if (this.props.onChange) this.props.onChange(item) }}
             >{item.label}</Item>
           )
         })}

+ 15 - 14
src/components/atoms/ToggleButtonBar/test.jsx

@@ -12,29 +12,30 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import ToggleButtonBar from '.'
 
-const wrap = props => shallow(<ToggleButtonBar {...props} />)
+const wrap = props => new TestWrapper(shallow(<ToggleButtonBar {...props} />), 'toggleButtonBar')
 const items = [
   { label: 'test1', value: 'test_1' },
   { label: 'test2', value: 'test_2' },
 ]
 
-it('renders the given items', () => {
-  let wrapper = wrap({ items })
-  let firstItemLabel = new DOMParser()
-    .parseFromString(wrapper.children().at(0).html(), 'text/xml').firstChild.innerHTML
-  let secondItemLabel = new DOMParser()
-    .parseFromString(wrapper.children().at(1).html(), 'text/xml').firstChild.innerHTML
-  expect(firstItemLabel).toBe('test1')
-  expect(secondItemLabel).toBe('test2')
-})
+describe('ToggleButtonBar Component', () => {
+  it('renders the given items', () => {
+    const wrapper = wrap({ items })
+    expect(wrapper.findText(items[0].value)).toBe(items[0].label)
+    expect(wrapper.findText(items[1].value)).toBe(items[1].label)
+  })
 
-it('selects the given value', () => {
-  let wrapper = wrap({ items, selectedValue: 'test_2' })
+  it('selects the given value', () => {
+    let wrapper = wrap({ items, selectedValue: 'test_2' })
 
-  expect(wrapper.children().at(0).prop('selected')).toBe(false)
-  expect(wrapper.children().at(1).prop('selected')).toBe(true)
+    expect(wrapper.find(items[0].value).prop('selected')).toBe(false)
+    expect(wrapper.find(items[1].value).prop('selected')).toBe(true)
+  })
 })

+ 1 - 1
src/components/molecules/DatetimePicker/index.jsx

@@ -211,7 +211,7 @@ class DatetimePicker extends React.Component<Props, State> {
       <Wrapper>
         <DropdownButtonStyled
           customRef={e => { this.buttonRef = e }}
-          data-test-id="dropdownButton"
+          data-test-id="datetimePicker-dropdownButton"
           width={207}
           value={(timezoneDate && moment(timezoneDate).format('DD/MM/YYYY hh:mm A')) || '-'}
           centered

+ 4 - 3
src/components/molecules/DatetimePicker/test.jsx

@@ -18,16 +18,17 @@ import React from 'react'
 import { shallow } from 'enzyme'
 import moment from 'moment'
 import sinon from 'sinon'
+import TestWrapper from '../../../utils/TestWrapper'
 import DatetimePicker from '.'
 
-const wrap = props => shallow(<DatetimePicker {...props} />)
+const wrap = props => new TestWrapper(shallow(<DatetimePicker {...props} />), 'datetimePicker')
 
 describe('DateTimePicker Component', () => {
   it('renders date value in dropdown label', () => {
     let onChange = sinon.spy()
     let wrapper = wrap({ value: new Date(2017, 3, 21, 14, 22), onChange })
     let label = '21/04/2017 02:22 PM'
-    expect(wrapper.children().at(0).prop('value')).toBe(label)
+    expect(wrapper.find('dropdownButton').prop('value')).toBe(label)
   })
 
   it('renders date value in UTC timezone in dropdown label', () => {
@@ -35,6 +36,6 @@ describe('DateTimePicker Component', () => {
     const date = new Date(2017, 3, 21, 14, 22)
     let wrapper = wrap({ value: date, onChange, timezone: 'utc' })
     const label = moment(date).add(new Date().getTimezoneOffset(), 'minutes').format('DD/MM/YYYY hh:mm A')
-    expect(wrapper.children().at(0).prop('value')).toBe(label)
+    expect(wrapper.find('dropdownButton').prop('value')).toBe(label)
   })
 })

+ 1 - 0
src/components/molecules/DetailsNavigation/index.jsx

@@ -47,6 +47,7 @@ class DetailsNavigation extends React.Component<Props> {
     return (
       this.props.items.map(item => (
         <Item
+          data-test-id={`detailsNavigation-${item.value}`}
           selected={item.value === this.props.selectedValue}
           key={item.value || item.label}
           href={this.props.customHref ? this.props.customHref(item) : `/#/${this.props.itemType || ''}${(item.value && '/') || ''}${item.value}/${this.props.itemId || ''}`}

+ 19 - 12
src/components/molecules/DetailsNavigation/test.jsx

@@ -12,28 +12,35 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import DetailsNavigation from '.'
 
-const wrap = props => shallow(<DetailsNavigation {...props} />)
+const wrap = props => new TestWrapper(shallow(<DetailsNavigation {...props} />), 'detailsNavigation')
 const items = [
   { label: 'Item 1', value: 'item-1' },
   { label: 'Item 2', value: 'item-2' },
   { label: 'Item 3', value: 'item-3' },
 ]
 
-it('renders 3 items', () => {
-  let wrapper = wrap({ items })
-  expect(wrapper.children().length).toBe(3)
-})
+describe('DetailsNavigation Component', () => {
+  it('renders 3 items', () => {
+    let wrapper = wrap({ items })
+    items.forEach(item => {
+      expect(wrapper.findText(item.value)).toBe(item.label)
+    })
+  })
 
-it('has items with correct href attribute', () => {
-  let wrapper = wrap({ items, itemType: 'replica', itemId: 'item-id' })
-  expect(wrapper.childAt(0).prop('href')).toBe('/#/replica/item-1/item-id')
-})
+  it('has items with correct href attribute', () => {
+    let wrapper = wrap({ items, itemType: 'replica', itemId: 'item-id' })
+    expect(wrapper.find(items[0].value).prop('href')).toBe('/#/replica/item-1/item-id')
+  })
 
-it('has items with correct href attribute, if items have no value', () => {
-  let wrapper = wrap({ items: [{ label: 'Item 1', value: '' }], itemType: 'migration', itemId: 'item-id' })
-  expect(wrapper.childAt(0).prop('href')).toBe('/#/migration/item-id')
+  it('has items with correct href attribute, if items have no value', () => {
+    let wrapper = wrap({ items: [{ label: 'Item 1', value: '' }], itemType: 'migration', itemId: 'item-id' })
+    expect(wrapper.find('').prop('href')).toBe('/#/migration/item-id')
+  })
 })

+ 3 - 0
src/components/molecules/Dropdown/index.jsx

@@ -110,6 +110,7 @@ type Props = {
   noSelectionMessage: string,
   disabled: boolean,
   width: number,
+  'data-test-id'?: string,
 }
 type State = {
   showDropdownList: boolean,
@@ -323,9 +324,11 @@ class Dropdown extends React.Component<Props, State> {
         className={this.props.className}
         onMouseDown={() => { this.itemMouseDown = true }}
         onMouseUp={() => { this.itemMouseDown = false }}
+        data-test-id={this.props['data-test-id'] || 'dropdown'}
       >
         <DropdownButton
           {...this.props}
+          data-test-id="dropdown-dropdownButton"
           innerRef={ref => { this.buttonRef = ref }}
           onMouseDown={() => { this.itemMouseDown = true }}
           onMouseUp={() => { this.itemMouseDown = false }}

+ 25 - 35
src/components/molecules/Dropdown/test.jsx

@@ -12,51 +12,41 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import Dropdown from '.'
 
-const wrap = props => shallow(<Dropdown {...props} />)
+const wrap = props => new TestWrapper(shallow(<Dropdown {...props} />), 'dropdown')
 const items = [
   { label: 'Item 1', value: 'item-1' },
   { label: 'Item 2', value: 'item-2' },
   { label: 'Item 3', value: 'item-3' },
 ]
 
-// it('opens list with the correct 3 items on button click', () => {
-//   let wrapper = wrap({ items })
-//   wrapper.childAt(0).simulate('click')
-//   let itemsList = wrapper.childAt(1).childAt(1)
-//   expect(itemsList.children().length).toBe(3)
-//   expect(itemsList.childAt(0).contains('Item 1')).toBe(true)
-//   expect(itemsList.childAt(1).contains('Item 2')).toBe(true)
-//   expect(itemsList.childAt(2).contains('Item 3')).toBe(true)
-// })
-
-// it('dispatches change on item click with correct argument', () => {
-//   let onChange = sinon.spy()
-//   let wrapper = wrap({ items, onChange })
-//   wrapper.childAt(0).simulate('click')
-//   let itemsList = wrapper.childAt(1).childAt(1)
-//   itemsList.childAt(1).simulate('click')
-//   expect(onChange.args[0][0].value).toBe('item-2')
-// })
+describe('Dropdown Component', () => {
+  it('renders selected item', () => {
+    let wrapper = wrap({ items, selectedItem: items[1] })
+    expect(wrapper.find('dropdownButton').prop('value')).toBe(items[1].label)
+    wrapper = wrap({ items, selectedItem: items[1].label })
+    expect(wrapper.find('dropdownButton').prop('value')).toBe(items[1].label)
+    wrapper = wrap({
+      items: [{ value: 'the_value', name: 'label' }],
+      selectedItem: { value: 'the_value', name: 'label' },
+      labelField: 'name',
+    })
+    expect(wrapper.find('dropdownButton').prop('value')).toBe('label')
+  })
 
-// it('uses labelField to render items', () => {
-//   let newItems = items.map(i => { return { value: i.value, name: i.label } })
-//   let wrapper = wrap({ items: newItems, labelField: 'name' })
-//   wrapper.childAt(0).simulate('click')
-//   let itemsList = wrapper.childAt(1).childAt(1)
-//   expect(itemsList.childAt(1).contains('Item 2')).toBe(true)
-// })
-
-it('renders no items message', () => {
-  let wrapper = wrap({ items: [], noItemsMessage: 'no items' })
-  expect(wrapper.childAt(0).prop('value')).toBe('no items')
-})
+  it('renders no items message', () => {
+    let wrapper = wrap({ items: [], noItemsMessage: 'no items' })
+    expect(wrapper.find('dropdownButton').prop('value')).toBe('no items')
+  })
 
-it('renders no selection message', () => {
-  let wrapper = wrap({ items, noSelectionMessage: 'no selection' })
-  expect(wrapper.childAt(0).prop('value')).toBe('no selection')
+  it('renders no selection message', () => {
+    let wrapper = wrap({ items, noSelectionMessage: 'no selection' })
+    expect(wrapper.find('dropdownButton').prop('value')).toBe('no selection')
+  })
 })

+ 2 - 0
src/components/molecules/DropdownFilter/index.jsx

@@ -125,6 +125,7 @@ class DropdownFilter extends React.Component<Props, State> {
       <List
         onMouseDown={() => { this.itemMouseDown = true }}
         onMouseUp={() => { this.itemMouseDown = false }}
+        data-test-id="dropdownFilter-list"
       >
         <Tip />
         <ListItems>
@@ -147,6 +148,7 @@ class DropdownFilter extends React.Component<Props, State> {
   renderButton() {
     return (
       <Button
+        data-test-id="dropdownFilter-button"
         onMouseDown={() => { this.itemMouseDown = true }}
         onMouseUp={() => { this.itemMouseDown = false }}
         onClick={() => { this.handleButtonClick() }}

+ 36 - 0
src/components/molecules/DropdownFilter/test.jsx

@@ -0,0 +1,36 @@
+/*
+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 TestWrapper from '../../../utils/TestWrapper'
+import DropdownFilter from '.'
+
+const wrap = props => new TestWrapper(shallow(<DropdownFilter {...props} />), 'dropdownFilter')
+const items = [
+  { label: 'Item 1', value: 'item-1' },
+  { label: 'Item 2', value: 'item-2' },
+  { label: 'Item 3', value: 'item-3' },
+]
+
+describe('DropdownFilter Component', () => {
+  it('opens filter list on button click', () => {
+    const wrapper = wrap({ items })
+    expect(wrapper.find('list').length).toBe(0)
+    wrapper.find('button').simulate('click')
+    expect(wrapper.find('list').length).toBe(1)
+  })
+})

+ 1 - 1
src/components/molecules/DropdownLink/index.jsx

@@ -351,7 +351,7 @@ class DropdownLink extends React.Component<Props, State> {
           onClick={() => this.handleButtonClick()}
           disabled={this.props.disabled}
         >
-          <Label innerRef={label => { this.labelRef = label }} data-test-id="dropdownLinkLabel">{renderLabel()}</Label>
+          <Label innerRef={label => { this.labelRef = label }} data-test-id="dropdownLink-label">{renderLabel()}</Label>
           <Arrow innerRef={arrow => { this.arrowRef = arrow }} />
         </LinkButton>
         {this.renderList()}

+ 3 - 21
src/components/molecules/DropdownLink/test.jsx

@@ -17,9 +17,10 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import React from 'react'
 import { shallow } from 'enzyme'
 import sinon from 'sinon'
+import TestWrapper from '../../../utils/TestWrapper'
 import DropdownLink from '.'
 
-const wrap = props => shallow(<DropdownLink {...props} />)
+const wrap = props => new TestWrapper(shallow(<DropdownLink {...props} />), 'dropdownLink')
 
 describe('DropdownLink Component', () => {
   it('renders with selectedItem', () => {
@@ -33,25 +34,6 @@ describe('DropdownLink Component', () => {
       selectedItem: 'item-2',
       onChange,
     })
-    expect(wrapper.findWhere(w => w.prop('data-test-id') === 'dropdownLinkLabel').dive().text()).toBe('Item 2')
+    expect(wrapper.findText('label')).toBe('Item 2')
   })
-
-  // it('has selected item highlighted when opening the list', () => {
-  //   let onChange = sinon.spy()
-  //   let wrapper = wrap({
-  //     items: [
-  //       { label: 'Item 1', value: 'item-1' },
-  //       { label: 'Item 2', value: 'item-2' },
-  //       { label: 'Item 3', value: 'item-3' },
-  //     ],
-  //     selectedItem: 'item-2',
-  //     onChange,
-  //   })
-  //   wrapper.childAt(0).simulate('click')
-  // let list = wrapper.childAt(1)
-  // expect(list.children().length).toBe(3)
-  // expect(list.childAt(1).prop('selected')).toBe(true)
-  // expect(list.childAt(0).prop('selected')).toBe(false)
-  // expect(list.childAt(2).prop('selected')).toBe(false)
-  // })
 })

+ 18 - 11
src/components/molecules/EndpointField/index.jsx

@@ -44,8 +44,8 @@ type Props = {
   name: string,
   type: string,
   value: any,
-  onChange: (value: any) => void,
-  className: string,
+  onChange?: (value: any) => void,
+  className?: string,
   minimum: number,
   maximum: number,
   password: boolean,
@@ -53,17 +53,17 @@ type Props = {
   large: boolean,
   highlight: boolean,
   disabled: boolean,
-  enum: string[],
+  enum?: string[],
 }
 @observer
 class Field extends React.Component<Props> {
   renderSwitch() {
     return (
       <Switch
-        dataTestId={`switch-${this.props.name}`}
+        data-test-id={`endpointField-switch-${this.props.name}`}
         disabled={this.props.disabled}
         checked={this.props.value || false}
-        onChange={checked => { this.props.onChange(checked) }}
+        onChange={checked => { if (this.props.onChange) this.props.onChange(checked) }}
       />
     )
   }
@@ -71,12 +71,13 @@ class Field extends React.Component<Props> {
   renderTextInput() {
     return (
       <TextInput
+        data-test-id={`endpointField-textInput-${this.props.name}`}
         required={this.props.required}
         highlight={this.props.highlight}
         type={this.props.password ? 'password' : 'text'}
         large={this.props.large}
         value={this.props.value}
-        onChange={e => { this.props.onChange(e.target.value) }}
+        onChange={e => { if (this.props.onChange) this.props.onChange(e.target.value) }}
         placeholder={LabelDictionary.get(this.props.name)}
         disabled={this.props.disabled}
       />
@@ -84,6 +85,10 @@ class Field extends React.Component<Props> {
   }
 
   renderEnumDropdown() {
+    if (!this.props.enum) {
+      return null
+    }
+
     let items = this.props.enum.map(e => {
       return {
         label: LabelDictionary.get(e),
@@ -94,10 +99,11 @@ class Field extends React.Component<Props> {
 
     return (
       <Dropdown
+        data-test-id={`endpointField-dropdown-${this.props.name}`}
         large={this.props.large}
         selectedItem={selectedItem}
         items={items}
-        onChange={item => this.props.onChange(item.value)}
+        onChange={item => { if (this.props.onChange) this.props.onChange(item.value) }}
         disabled={this.props.disabled}
       />
     )
@@ -115,11 +121,11 @@ class Field extends React.Component<Props> {
 
     return (
       <Dropdown
-        data-test-id={`dropdown-${this.props.name}`}
+        data-test-id={`endpointField-dropdown-${this.props.name}`}
         large={this.props.large}
         selectedItem={this.props.value}
         items={items}
-        onChange={item => this.props.onChange(item.value)}
+        onChange={item => { if (this.props.onChange) this.props.onChange(item.value) }}
         disabled={this.props.disabled}
       />
     )
@@ -128,9 +134,10 @@ class Field extends React.Component<Props> {
   renderRadioInput() {
     return (
       <RadioInput
+        data-test-id={`endpointField-radioInput-${this.props.name}`}
         checked={this.props.value}
         label={LabelDictionary.get(this.props.name)}
-        onChange={e => this.props.onChange(e.target.checked)}
+        onChange={e => { if (this.props.onChange) this.props.onChange(e.target.checked) }}
         disabled={this.props.disabled}
       />
     )
@@ -170,7 +177,7 @@ class Field extends React.Component<Props> {
 
     return (
       <Label>
-        <LabelText>{LabelDictionary.get(this.props.name)}</LabelText>
+        <LabelText data-test-id="endpointField-label">{LabelDictionary.get(this.props.name)}</LabelText>
         {infoIcon}
       </Label>
     )

+ 61 - 47
src/components/molecules/EndpointField/test.jsx

@@ -12,63 +12,77 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import EndpointField from '.'
 
-const wrap = props => shallow(<EndpointField {...props} />)
+// $FlowIgnore
+const wrap = props => new TestWrapper(shallow(<EndpointField {...props} />), 'endpointField')
 
-it('renders boolean field with correct value', () => {
-  let wrapper = wrap({ type: 'boolean', name: 'label', value: true })
-  expect(wrapper.childAt(1).name()).toBe('Switch')
-  expect(wrapper.childAt(1).prop('checked')).toBe(true)
-})
+describe('EndpointField Component', () => {
+  it('renders label', () => {
+    const wrapper = wrap({ type: 'boolean', value: true, name: 'the_name' })
+    expect(wrapper.findText('label')).toBe('The Name')
+  })
 
-it('renders boolean field disabled', () => {
-  let wrapper = wrap({ type: 'boolean', name: 'label', disabled: true })
-  expect(wrapper.childAt(1).prop('disabled')).toBe(true)
-})
+  it('renders boolean field with correct value', () => {
+    let wrapper = wrap({ type: 'boolean', name: 'the_name', value: true })
+    expect(wrapper.find('switch-the_name').length).toBe(1)
+    expect(wrapper.find('switch-the_name').prop('checked')).toBe(true)
+  })
 
-it('renders text input field with correct label and value', () => {
-  let wrapper = wrap({ type: 'string', name: 'field_label', value: 'text-input' })
-  expect(wrapper.childAt(0).contains('Field Label')).toBe(true)
-  expect(wrapper.childAt(1).name()).toBe('TextInput')
-  expect(wrapper.childAt(1).prop('value')).toBe('text-input')
-})
+  it('renders boolean field disabled', () => {
+    let wrapper = wrap({ type: 'boolean', name: 'the_name', disabled: true })
+    expect(wrapper.find('switch-the_name').prop('disabled')).toBe(true)
+  })
 
-it('renders text input field with password, large, disabled, highlighted and required', () => {
-  let wrapper = wrap({
-    type: 'string',
-    name: 'field-label',
-    value: 'text-input',
-    password: true,
-    large: true,
-    disabled: true,
-    highlight: true,
-    required: true,
+  it('renders text input field with correct label and value', () => {
+    let wrapper = wrap({ type: 'string', name: 'the_name', value: 'the_value' })
+    expect(wrapper.findText('label')).toBe('The Name')
+    expect(wrapper.find('textInput-the_name').length).toBe(1)
+    expect(wrapper.find('textInput-the_name').prop('value')).toBe('the_value')
   })
-  expect(wrapper.childAt(1).prop('type')).toBe('password')
-  expect(wrapper.childAt(1).prop('large')).toBe(true)
-  expect(wrapper.childAt(1).prop('disabled')).toBe(true)
-  expect(wrapper.childAt(1).prop('highlight')).toBe(true)
-  expect(wrapper.childAt(1).prop('required')).toBe(true)
-})
 
-it('renders integer dropdown field with correct items', () => {
-  let wrapper = wrap({
-    type: 'integer',
-    name: 'field-label',
-    value: 11,
-    minimum: 10,
-    maximum: 15,
+  it('renders text input field with password, large, disabled, highlighted and required', () => {
+    let wrapper = wrap({
+      type: 'string',
+      name: 'the_name',
+      value: 'the_value',
+      password: true,
+      large: true,
+      disabled: true,
+      highlight: true,
+      required: true,
+    })
+    let textInput = wrapper.find('textInput-the_name')
+    expect(textInput.prop('type')).toBe('password')
+    expect(textInput.prop('large')).toBe(true)
+    expect(textInput.prop('disabled')).toBe(true)
+    expect(textInput.prop('highlight')).toBe(true)
+    expect(textInput.prop('required')).toBe(true)
   })
-  expect(wrapper.childAt(1).prop('selectedItem')).toBe(11)
-  expect(wrapper.childAt(1).prop('items')[3].value).toBe(13)
-  expect(wrapper.childAt(1).prop('items')[5].value).toBe(15)
-})
 
-it('renders radio input field with correct value', () => {
-  let wrapper = wrap({ type: 'radio', name: 'label', value: true })
-  expect(wrapper.childAt(0).name()).toBe('RadioInput')
-  expect(wrapper.childAt(0).prop('checked')).toBe(true)
+  it('renders integer dropdown field with correct items', () => {
+    let wrapper = wrap({
+      type: 'integer',
+      name: 'the_name',
+      value: 11,
+      minimum: 10,
+      maximum: 15,
+    })
+    let dropdown = wrapper.find('dropdown-the_name')
+    expect(dropdown.prop('selectedItem')).toBe(11)
+    expect(dropdown.prop('items')[3].value).toBe(13)
+    expect(dropdown.prop('items')[5].value).toBe(15)
+  })
+
+  it('renders radio input field with correct value', () => {
+    let wrapper = wrap({ type: 'radio', name: 'the_name', value: true })
+    let radioInput = wrapper.find('radioInput-the_name')
+    expect(radioInput.length).toBe(1)
+    expect(radioInput.prop('checked')).toBe(true)
+  })
 })

+ 4 - 4
src/components/molecules/EndpointListItem/index.jsx

@@ -109,11 +109,11 @@ class EndpointListItem extends React.Component<Props> {
           checked={this.props.selected}
           onChange={this.props.onSelectedChange}
         />
-        <Content onClick={this.props.onClick} data-test-id={`endpointListItemContent-${this.props.item.name}`}>
+        <Content onClick={this.props.onClick} data-test-id={`endpointListItem-content-${this.props.item.name}`}>
           <Image image={endpointImage} />
           <Title>
-            <TitleLabel>{this.props.item.name}</TitleLabel>
-            <Subtitle>{this.props.item.description || 'N/A'}</Subtitle>
+            <TitleLabel data-test-id="endpointListItem-name">{this.props.item.name}</TitleLabel>
+            <Subtitle data-test-id="endpointListItem-description">{this.props.item.description || 'N/A'}</Subtitle>
           </Title>
           <EndpointLogos height={42} endpoint={this.props.item.type} />
           <Created>
@@ -124,7 +124,7 @@ class EndpointListItem extends React.Component<Props> {
           </Created>
           <Usage>
             <ItemLabel>Usage</ItemLabel>
-            <ItemValue>
+            <ItemValue data-test-id="endpointListItem-usageCount">
               {this.props.getUsage(this.props.item).migrationsCount} migrations,&nbsp;
               {this.props.getUsage(this.props.item).replicasCount} replicas
             </ItemValue>

+ 27 - 19
src/components/molecules/EndpointListItem/test.jsx

@@ -12,33 +12,41 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import EndpointListItem from '.'
 
-const wrap = props => shallow(<EndpointListItem {...props} />)
+const wrap = props => new TestWrapper(shallow(
+  // $FlowIgnore
+  (<EndpointListItem {...props} />)
+), 'endpointListItem')
 
-it('renders item properties', () => {
-  let wrapper = wrap({
-    item: { name: 'name-to-test', description: 'description-to-test' },
-    getUsage: () => { return {} },
+describe('EndpointListItem Component', () => {
+  it('renders item properties', () => {
+    let wrapper = wrap({
+      item: { name: 'name-to-test', description: 'description-to-test' },
+      getUsage: () => { return {} },
+    })
+    expect(wrapper.findText('name')).toBe('name-to-test')
+    expect(wrapper.findText('description')).toBe('description-to-test')
   })
-  expect(wrapper.contains('name-to-test')).toBe(true)
-  expect(wrapper.contains('description-to-test')).toBe(true)
-})
 
-it('renders usage count', () => {
-  let wrapper = wrap({
-    item: {},
-    getUsage: () => { return { replicasCount: 12, migrationsCount: 11 } },
+  it('renders usage count', () => {
+    let wrapper = wrap({
+      item: {},
+      getUsage: () => { return { replicasCount: 12, migrationsCount: 11 } },
+    })
+    expect(wrapper.findText('usageCount')).toBe('11 migrations, 12 replicas')
   })
-  expect(wrapper.html().indexOf('11 migrations, 12 replicas') > -1).toBe(true)
-})
 
-it('dispatches onClick', () => {
-  let onClick = sinon.spy()
-  let wrapper = wrap({ item: {}, getUsage: () => { return {} }, onClick })
-  wrapper.childAt(1).simulate('click')
-  expect(onClick.calledOnce).toBe(true)
+  it('dispatches onClick', () => {
+    let onClick = sinon.spy()
+    let wrapper = wrap({ item: { name: 't' }, getUsage: () => { return {} }, onClick })
+    wrapper.find('content-t').simulate('click')
+    expect(onClick.calledOnce).toBe(true)
+  })
 })

+ 1 - 1
src/components/molecules/LoadingButton/index.jsx

@@ -45,7 +45,7 @@ class LoadingButton extends React.Component<Props> {
   render() {
     return (
       <ButtonStyled {...this.props} disabled>
-        <span>{this.props.children}<Loading /></span>
+        <span data-test-id="loadingButton-label">{this.props.children}<Loading /></span>
       </ButtonStyled>
     )
   }

+ 9 - 4
src/components/molecules/LoadingButton/test.jsx

@@ -12,12 +12,17 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import LoadingButton from '.'
 
-it('renders disabled with given label', () => {
-  let wrapper = shallow(<LoadingButton>Loading ...</LoadingButton>)
-  expect(wrapper.prop('disabled')).toBe(true)
-  expect(wrapper.childAt(0).contains('Loading ...')).toBe(true)
+describe('LoadingButton Component', () => {
+  it('renders disabled with given label', () => {
+    let wrapper = new TestWrapper(shallow(<LoadingButton>Loading ...</LoadingButton>), 'loadingButton')
+    expect(wrapper.prop('disabled')).toBe(true)
+    expect(wrapper.findText('label', true)).toBe('Loading ...<styled.span />')
+  })
 })

+ 2 - 2
src/components/molecules/LoginFormField/index.jsx

@@ -42,8 +42,8 @@ type Props = {
 const LoginFormField = (props: Props) => {
   return (
     <Wrapper>
-      <FormFieldLabel>{props.label}</FormFieldLabel>
-      <StyledTextInput {...props} onChange={props.onChange} />
+      <FormFieldLabel data-test-id="loginFormField-label">{props.label}</FormFieldLabel>
+      <StyledTextInput data-test-id="loginFormField-input" {...props} onChange={props.onChange} />
     </Wrapper>
   )
 }

+ 20 - 4
src/components/molecules/LoginFormField/test.jsx

@@ -12,13 +12,29 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import LoginFormField from '.'
 
-const wrap = props => shallow(<LoginFormField {...props} />)
+const wrap = props => new TestWrapper(shallow(
+  // $FlowIgnore
+  <LoginFormField {...props} />
+), 'loginFormField')
+
+describe('LoginFormField Component', () => {
+  it('renders with correct label', () => {
+    let wrapper = wrap({ label: 'Username' })
+    expect(wrapper.findText('label')).toBe('Username')
+  })
 
-it('renders with correct label', () => {
-  let wrapper = wrap({ label: 'Username' })
-  expect(wrapper.childAt(0).contains('Username')).toBe(true)
+  it('dispatches change on input change', () => {
+    const onChange = sinon.spy()
+    let wrapper = wrap({ label: 'Username', onChange })
+    wrapper.find('input').simulate('change', { t: 't' })
+    expect(onChange.args[0][0].t).toBe('t')
+  })
 })

+ 6 - 2
src/components/molecules/LoginOptions/index.jsx

@@ -108,8 +108,12 @@ const LoginOptions = (props: Props) => {
     <Wrapper>
       {buttons.map((button) => {
         return (
-          <Button key={button.id} id={button.id}>
-            <Logo id={button.id} />Sign in with {button.name}
+          <Button
+            data-test-id={`loginOptions-button-${button.id}`}
+            key={button.id}
+            id={button.id}
+          >
+            <Logo data-test-id={`loginOptions-logo-${button.id}`} id={button.id} />Sign in with {button.name}
           </Button>
         )
       })}

+ 8 - 5
src/components/molecules/LoginOptions/test.jsx

@@ -16,9 +16,10 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import React from 'react'
 import { shallow } from 'enzyme'
+import TestWrapper from '../../../utils/TestWrapper'
 import LoginOptions from '.'
 
-const wrap = props => shallow(<LoginOptions {...props} />)
+const wrap = props => new TestWrapper(shallow(<LoginOptions {...props} />), 'loginOptions')
 
 let buttons = [
   {
@@ -44,10 +45,12 @@ let buttons = [
 ]
 
 describe('LoginOptions Component', () => {
-  it('renders with given buttons', () => {
+  it('renders with all buttons', () => {
     let wrapper = wrap({ buttons })
-    expect(wrapper.children().length).toBe(4)
-    expect(wrapper.childAt(2).prop('id')).toBe('facebook')
-    expect(wrapper.childAt(1).html().indexOf('Sign in with Microsoft')).toBeGreaterThan(-1)
+    expect(wrapper.find('button', true).length).toBe(4)
+    buttons.forEach(button => {
+      expect(wrapper.findText(`button-${button.id}`)).toBe(`<styled.div />Sign in with ${button.name}`)
+      expect(wrapper.find(`logo-${button.id}`).prop('id')).toBe(button.id)
+    })
   })
 })

+ 8 - 2
src/components/molecules/MainListFilter/index.jsx

@@ -106,6 +106,7 @@ class MainListFilter extends React.Component<Props> {
         {this.props.items.map(item => {
           return (
             <FilterItem
+              data-test-id={`mainListFilter-filterItem-${item.value}`}
               onClick={() => this.props.onFilterItemClick(item)}
               key={item.value}
               selected={this.props.selectedValue === item.value}
@@ -124,11 +125,12 @@ class MainListFilter extends React.Component<Props> {
 
     return (
       <Selection>
-        <SelectionText>
+        <SelectionText data-test-id="mainListFilter-selectionText">
           {this.props.selectionInfo.selected} of {this.props.selectionInfo.total}&nbsp;
           {this.props.selectionInfo.label}(s) selected
         </SelectionText>
         <Dropdown
+          data-test-id="mainListFilter-dropdown"
           noSelectionMessage="Select an action"
           items={this.props.actions}
           onChange={item => { this.props.onActionChange(item.value) }}
@@ -155,7 +157,11 @@ class MainListFilter extends React.Component<Props> {
         <Main>
           {renderCheckbox()}
           {this.renderFilterGroup()}
-          <ReloadButton style={{ marginRight: '16px' }} onClick={this.props.onReloadButtonClick} />
+          <ReloadButton
+            data-test-id="mainListFilter-reloadButton"
+            style={{ marginRight: '16px' }}
+            onClick={this.props.onReloadButtonClick}
+          />
           <SearchInput onChange={this.props.onSearchChange} value={this.props.searchValue} />
         </Main>
         {this.renderSelectionInfo()}

+ 38 - 27
src/components/molecules/MainListFilter/test.jsx

@@ -12,12 +12,18 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import MainListFilter from '.'
 
-const wrap = props => shallow(<MainListFilter {...props} />)
+const wrap = props => new TestWrapper(shallow(
+  // $FlowIgnore
+  <MainListFilter {...props} />
+), 'mainListFilter')
 
 let items = [
   { label: 'Item 1', value: 'item-1' },
@@ -32,34 +38,39 @@ let actions = [
 
 let selectionInfo = { selected: 2, total: 7, label: 'items' }
 
-it('renders given items', () => {
-  let wrapper = wrap({ items, actions, selectionInfo })
-  expect(wrapper.childAt(0).childAt(1).children().length).toBe(3)
-  expect(wrapper.childAt(0).childAt(1).childAt(1).html().indexOf('Item 2')).toBeGreaterThan(-1)
-})
+describe('MainListFilter Component', () => {
+  it('renders given items', () => {
+    let wrapper = wrap({ items, actions, selectionInfo })
+    expect(wrapper.find('filterItem', true).length).toBe(items.length)
+    items.forEach(item => {
+      expect(wrapper.findText(`filterItem-${item.value}`)).toBe(item.label)
+    })
+  })
 
-it('renders given actions', () => {
-  let wrapper = wrap({ items, actions, selectionInfo })
-  expect(wrapper.find('Dropdown').prop('items').length).toBe(2)
-  expect(wrapper.find('Dropdown').prop('items')[1].value).toBe('action-2')
-})
+  it('renders given actions', () => {
+    let wrapper = wrap({ items, actions, selectionInfo })
+    expect(wrapper.find('dropdown').prop('items').length).toBe(actions.length)
+    actions.forEach((action, i) => {
+      expect(wrapper.find('dropdown').prop('items')[i].value).toBe(action.value)
+    })
+  })
 
-it('renders selection info', () => {
-  let wrapper = wrap({ items, actions, selectionInfo })
-  expect(wrapper.childAt(1).childAt(0).html().indexOf('2 of 7 items(s) selected')).toBeGreaterThan(-1)
-})
+  it('renders selection info', () => {
+    let wrapper = wrap({ items, actions, selectionInfo })
+    expect(wrapper.findText('selectionText')).toBe('2 of 7 items(s) selected')
+  })
 
-it('handles reload click', () => {
-  let onReloadButtonClick = sinon.spy()
-  let wrapper = wrap({ items, actions, selectionInfo, onReloadButtonClick }).find('ReloadButton')
-  wrapper.simulate('click')
-  expect(onReloadButtonClick.calledOnce).toBe(true)
-})
+  it('handles reload click', () => {
+    let onReloadButtonClick = sinon.spy()
+    let wrapper = wrap({ items, actions, selectionInfo, onReloadButtonClick })
+    wrapper.find('reloadButton').simulate('click')
+    expect(onReloadButtonClick.calledOnce).toBe(true)
+  })
 
-it('handles item click with correct args', () => {
-  let onFilterItemClick = sinon.spy()
-  let wrapper = wrap({ items, actions, selectionInfo, onFilterItemClick })
-  let item = wrapper.childAt(0).childAt(1).childAt(2)
-  item.simulate('click')
-  expect(onFilterItemClick.args[0][0].value).toBe('item-3')
+  it('handles item click with correct args', () => {
+    let onFilterItemClick = sinon.spy()
+    let wrapper = wrap({ items, actions, selectionInfo, onFilterItemClick })
+    wrapper.find(`filterItem-${items[2].value}`).simulate('click')
+    expect(onFilterItemClick.args[0][0].value).toBe(items[2].value)
+  })
 })

+ 7 - 5
src/components/molecules/MainListItem/index.jsx

@@ -29,7 +29,7 @@ import type { Execution } from '../../../types/Execution'
 
 import arrowImage from './images/arrow.svg'
 
-const CheckboxStyled = styled(Checkbox) `
+const CheckboxStyled = styled(Checkbox)`
   opacity: ${props => props.checked ? 1 : 0};
   transition: all ${StyleProps.animations.swift};
 `
@@ -188,22 +188,24 @@ class MainListItem extends React.Component<Props> {
     let destinationType = this.props.endpointType(this.props.item.destination_endpoint_id)
     let endpointImages = (
       <EndpointsImages>
-        <EndpointLogos height={32} endpoint={sourceType} />
+        <EndpointLogos data-test-id="mainListItem-sourceLogo" height={32} endpoint={sourceType} />
         <EndpointImageArrow />
-        <EndpointLogos height={32} endpoint={destinationType} />
+        <EndpointLogos data-test-id="mainListItem-destLogo" height={32} endpoint={destinationType} />
       </EndpointsImages>
     )
+    const status = this.getStatus()
     return (
       <Wrapper>
         <CheckboxStyled
+          data-test-id="mainListItem-checkbox"
           checked={this.props.selected}
           onChange={this.props.onSelectedChange}
         />
-        <Content onClick={this.props.onClick} data-test-id="mainListItem">
+        <Content onClick={this.props.onClick} data-test-id="mainListItem-content">
           <Image image={this.props.image} />
           <Title>
             <TitleLabel>{this.props.item.instances[0]}</TitleLabel>
-            {this.getStatus() ? <StatusPill status={this.getStatus()} /> : null}
+            {status ? <StatusPill data-test-id={`mainListItem-statusPill-${status}`} status={status} /> : null}
           </Title>
           {endpointImages}
           {this.renderLastExecution()}

+ 27 - 19
src/components/molecules/MainListItem/test.jsx

@@ -12,12 +12,18 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import MainListItem from '.'
 
-const wrap = props => shallow(<MainListItem {...props} />)
+const wrap = props => new TestWrapper(shallow(
+  // $FlowIgnore
+  <MainListItem {...props} />
+), 'mainListItem')
 
 let item = {
   origin_endpoint_id: 'openstack',
@@ -27,25 +33,27 @@ let item = {
 }
 let endpointType = id => id
 
-it('renders with given status', () => {
-  let wrapper = wrap({ item, endpointType })
-  expect(wrapper.find('StatusPill').prop('status')).toBe('COMPLETED')
-})
+describe('MainListItem Component', () => {
+  it('renders with given status', () => {
+    let wrapper = wrap({ item, endpointType })
+    expect(wrapper.find('statusPill', true).at(0).prop('status')).toBe('COMPLETED')
+  })
 
-it('renders with given endpoints', () => {
-  let wrapper = wrap({ item, endpointType })
-  expect(wrapper.find('EndpointLogos').at(0).prop('endpoint')).toBe('openstack')
-  expect(wrapper.find('EndpointLogos').at(1).prop('endpoint')).toBe('azure')
-})
+  it('renders with given endpoints', () => {
+    let wrapper = wrap({ item, endpointType })
+    expect(wrapper.find('sourceLogo').prop('endpoint')).toBe(item.origin_endpoint_id)
+    expect(wrapper.find('destLogo').prop('endpoint')).toBe(item.destination_endpoint_id)
+  })
 
-it('renders with selected', () => {
-  let wrapper = wrap({ item, endpointType, selected: true })
-  expect(wrapper.find('Styled(Checkbox)').prop('checked')).toBe(true)
-})
+  it('renders with selected', () => {
+    let wrapper = wrap({ item, endpointType, selected: true })
+    expect(wrapper.find('checkbox').prop('checked')).toBe(true)
+  })
 
-it('dispatched item click', () => {
-  let onClick = sinon.spy()
-  let wrapper = wrap({ item, endpointType, onClick })
-  wrapper.childAt(1).simulate('click')
-  expect(onClick.calledOnce).toBe(true)
+  it('dispatched item click', () => {
+    let onClick = sinon.spy()
+    let wrapper = wrap({ item, endpointType, onClick })
+    wrapper.find('content').simulate('click')
+    expect(onClick.calledOnce).toBe(true)
+  })
 })

+ 1 - 1
src/components/molecules/Modal/index.jsx

@@ -147,7 +147,7 @@ class NewModal extends React.Component<Props> {
       return null
     }
 
-    return <Title>{this.props.title}</Title>
+    return <Title data-test-id="modal-title">{this.props.title}</Title>
   }
 
   render() {

+ 16 - 11
src/components/molecules/Modal/test.jsx

@@ -12,21 +12,26 @@ 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 TestWrapper from '../../../utils/TestWrapper'
 import Modal from '.'
 
-const wrap = props => shallow(<Modal {...props} />)
+const wrap = props => new TestWrapper(shallow(<Modal {...props} />), 'modal')
 
-it('renders open with title', () => {
-  let wrapper = wrap({ isOpen: true, children: <div>Modal</div>, title: 'title' })
-  expect(wrapper.childAt(0).contains('title')).toBe(true)
-  expect(wrapper.prop('contentLabel')).toBe('title')
-  expect(wrapper.prop('isOpen')).toBe(true)
-})
+describe('Modal Component', () => {
+  it('renders open with title', () => {
+    let wrapper = wrap({ isOpen: true, children: <div>Modal</div>, title: 'the_title' })
+    expect(wrapper.findText('title')).toBe('the_title')
+    expect(wrapper.prop('contentLabel')).toBe('the_title')
+    expect(wrapper.prop('isOpen')).toBe(true)
+  })
 
-it('renders children and add resize handler', () => {
-  let wrapper = wrap({ isOpen: true, children: <div>Modal</div>, title: 'title' })
-  expect(wrapper.childAt(1).html().indexOf('Modal')).toBeGreaterThan(-1)
-  expect(wrapper.childAt(1).prop('onResizeUpdate')).toBeTruthy()
+  it('renders children and add resize handler', () => {
+    let wrapper = wrap({ isOpen: true, children: <div data-test-id="modal-child">Modal</div>, title: 'the_title' })
+    expect(wrapper.findText('child', true)).toBe('Modal')
+    expect(wrapper.find('child').prop('onResizeUpdate')).toBeTruthy()
+  })
 })

+ 2 - 0
src/components/molecules/NewItemDropdown/index.jsx

@@ -188,6 +188,7 @@ class NewItemDropdown extends React.Component<Props, State> {
         {items.map(item => {
           return (
             <ListItem
+              data-test-id={`newItemDropdown-listItem-${item.title}`}
               key={item.title}
               onMouseDown={() => { this.itemMouseDown = true }}
               onMouseUp={() => { this.itemMouseDown = false }}
@@ -212,6 +213,7 @@ class NewItemDropdown extends React.Component<Props, State> {
     return (
       <Wrapper>
         <DropdownButton
+          data-test-id="newItemDropdown-button"
           onMouseDown={() => { this.itemMouseDown = true }}
           onMouseUp={() => { this.itemMouseDown = false }}
           onClick={() => this.handleButtonClick()}

+ 18 - 14
src/components/molecules/NewItemDropdown/test.jsx

@@ -12,25 +12,29 @@ 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 NewItemDropdown from '.'
 
-const wrap = props => shallow(<NewItemDropdown {...props} />)
+const wrap = props => new TW(shallow(<NewItemDropdown onChange={() => { }} {...props} />), 'newItemDropdown')
 
-it('opens list on click', () => {
-  let wrapper = wrap()
-  expect(wrapper.children().length).toBe(1)
-  wrapper.childAt(0).simulate('click')
-  expect(wrapper.children().length).toBe(2)
-  expect(wrapper.childAt(1).children().length).toBe(3)
-})
+describe('NewItemDropdown Component', () => {
+  it('opens list on click', () => {
+    let wrapper = wrap()
+    expect(wrapper.find('listItem', true).length).toBe(0)
+    wrapper.find('button').simulate('click')
+    expect(wrapper.find('listItem', true).length).toBe(3)
+  })
 
-it('dispatches change on item click with correct args', () => {
-  let onChange = sinon.spy()
-  let wrapper = wrap({ onChange })
-  wrapper.childAt(0).simulate('click')
-  wrapper.childAt(1).childAt(2).simulate('click')
-  expect(onChange.args[0][0].value).toBe('endpoint')
+  it('dispatches change on item click with correct args', () => {
+    let onChange = sinon.spy()
+    let wrapper = wrap({ onChange })
+    wrapper.find('button').simulate('click')
+    wrapper.find('listItem-Endpoint').simulate('click')
+    expect(onChange.args[0][0].value).toBe('endpoint')
+  })
 })

+ 8 - 6
src/components/molecules/NotificationDropdown/index.jsx

@@ -136,7 +136,7 @@ const Description = styled.div``
 const NoItems = styled.div`
   text-align: center;
 `
-
+const baseId = 'notificationDropdown'
 type Props = {
   white?: boolean,
   items: NotificationItem[],
@@ -201,7 +201,7 @@ class NotificationDropdown extends React.Component<Props, State> {
           onMouseDown={() => { this.itemMouseDown = true }}
           onMouseUp={() => { this.itemMouseDown = false }}
         >
-          <NoItems>There are no notifications</NoItems>
+          <NoItems data-test-id="notificationDropdown-noItems">There are no notifications</NoItems>
         </ListItem>
       </List>
     )
@@ -220,17 +220,18 @@ class NotificationDropdown extends React.Component<Props, State> {
 
           return (
             <ListItem
+              data-test-id={`${baseId}-item-${item.id || new Date().getTime().toString()}`}
               key={item.id}
               onMouseDown={() => { this.itemMouseDown = true }}
               onMouseUp={() => { this.itemMouseDown = false }}
               onClick={() => { this.handleItemClick() }}
             >
               <Title>
-                <TypeIcon level={item.level} />
-                <TitleLabel>{title}</TitleLabel>
-                <Time>{moment(Number(item.id)).format('HH:mm')}</Time>
+                <TypeIcon data-test-id={`${baseId}-itemLevel`} level={item.level} />
+                <TitleLabel data-test-id={`${baseId}-itemTitle`}>{title}</TitleLabel>
+                <Time data-test-id={`${baseId}-itemTime`}>{moment(Number(item.id)).format('HH:mm')}</Time>
               </Title>
-              <Description>{message}</Description>
+              <Description data-test-id={`${baseId}-itemDescription`}>{message}</Description>
             </ListItem>
           )
         })}
@@ -248,6 +249,7 @@ class NotificationDropdown extends React.Component<Props, State> {
 
     return (
       <Icon
+        data-test-id="notificationDropdown-button"
         onMouseDown={() => { this.itemMouseDown = true }}
         onMouseUp={() => { this.itemMouseDown = false }}
         onClick={() => this.handleButtonClick()}

+ 33 - 25
src/components/molecules/NotificationDropdown/test.jsx

@@ -12,12 +12,19 @@ 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 moment from 'moment'
+import TW from '../../../utils/TestWrapper'
 import NotificationDropdown from '.'
 
-const wrap = props => shallow(<NotificationDropdown {...props} />)
+const wrap = props => new TW(shallow(
+  // $FlowIgnore
+  <NotificationDropdown {...props} />
+), 'notificationDropdown')
 
 let items = [
   {
@@ -40,30 +47,31 @@ let items = [
   },
 ]
 
-it('renders no items message on click', () => {
-  let wrapper = wrap({ onClose: () => { } })
-  expect(wrapper.children().length).toBe(1)
-  wrapper.childAt(0).simulate('click')
-  expect(wrapper.childAt(1).html().indexOf('There are no notifications')).toBeGreaterThan(-1)
-})
+describe('NotificationDropdown Component', () => {
+  it('renders no items message on click', () => {
+    let wrapper = wrap({ onClose: () => { } })
+    expect(wrapper.find('noItems').length).toBe(0)
+    wrapper.find('button').simulate('click')
+    expect(wrapper.find('noItems').length).toBe(1)
+  })
 
-it('renders items correctly', () => {
-  let wrapper = wrap({ items, onClose: () => { } })
-  expect(wrapper.children().length).toBe(1)
-  wrapper.childAt(0).simulate('click')
-  let itemsWrapper = wrapper.childAt(1)
-  expect(itemsWrapper.findWhere(w => w.prop('level') === 'success').length).toBe(1)
-  expect(itemsWrapper.findWhere(w => w.prop('level') === 'info').length).toBe(1)
-  expect(itemsWrapper.findWhere(w => w.prop('level') === 'error').length).toBe(1)
-  expect(itemsWrapper.childAt(1).html().indexOf('Incrementally replicate virtual machines')).toBeGreaterThan(-1)
-})
+  it('renders items correctly', () => {
+    let wrapper = wrap({ items, onClose: () => { } })
+    wrapper.find('button').simulate('click')
+
+    items.forEach(item => {
+      expect(wrapper.find(`item-${item.id}`).find('itemLevel').prop('level')).toBe(item.level)
+      expect(wrapper.find(`item-${item.id}`).findText('itemTitle')).toBe(item.options.persistInfo.title)
+      expect(wrapper.find(`item-${item.id}`).findText('itemDescription')).toBe(item.message)
+      expect(wrapper.find(`item-${item.id}`).findText('itemTime')).toBe(moment(Number(item.id)).format('HH:mm'))
+    })
+  })
 
-it('dispatches onClose', () => {
-  let onClose = sinon.spy()
-  let wrapper = wrap({ items, onClose })
-  expect(wrapper.children().length).toBe(1)
-  wrapper.childAt(0).simulate('click')
-  let itemsWrapper = wrapper.childAt(1)
-  itemsWrapper.childAt(2).simulate('click')
-  expect(onClose.calledOnce).toBe(true)
+  it('dispatches onClose', () => {
+    let onClose = sinon.spy()
+    let wrapper = wrap({ items, onClose })
+    wrapper.find('button').simulate('click')
+    wrapper.find(`item-${items[0].id}`).simulate('click')
+    expect(onClose.calledOnce).toBe(true)
+  })
 })

+ 7 - 4
src/components/molecules/PropertiesTable/index.jsx

@@ -58,12 +58,12 @@ const Row = styled.div`
     border-bottom-left-radius: ${StyleProps.borderRadius};
   }
 `
-
+const baseId = 'propertiesTable'
 type Props = {
   properties: Field[],
   onChange: (property: Field, value: any) => void,
   valueCallback: (property: Field) => any,
- }
+}
 @observer
 class PropertiesTable extends React.Component<Props> {
   getName(propName: string): string {
@@ -76,6 +76,7 @@ class PropertiesTable extends React.Component<Props> {
   renderSwitch(prop: Field, opts: { triState: boolean }) {
     return (
       <Switch
+        data-test-id={`${baseId}-switch-${prop.name}`}
         secondary
         triState={opts.triState}
         height={16}
@@ -88,6 +89,7 @@ class PropertiesTable extends React.Component<Props> {
   renderTextInput(prop: Field) {
     return (
       <TextInput
+        data-test-id={`${baseId}-textInput-${prop.name}`}
         width="100%"
         embedded
         value={this.props.valueCallback(prop)}
@@ -118,6 +120,7 @@ class PropertiesTable extends React.Component<Props> {
 
     return (
       <Dropdown
+        data-test-id={`${baseId}-dropdown-${prop.name}`}
         width={320}
         noSelectionMessage="Choose a value"
         selectedItem={selectedItem ? selectedItem.label : null}
@@ -154,8 +157,8 @@ class PropertiesTable extends React.Component<Props> {
       <Wrapper>
         {this.props.properties.map(prop => {
           return (
-            <Row key={prop.name}>
-              <Column header>{this.getName(prop.name)}</Column>
+            <Row key={prop.name} data-test-id={`${baseId}-row-${prop.name}`}>
+              <Column header data-test-id={`${baseId}-header`}>{this.getName(prop.name)}</Column>
               <Column input>{this.renderInput(prop)}</Column>
             </Row>
           )

+ 38 - 22
src/components/molecules/PropertiesTable/test.jsx

@@ -12,37 +12,53 @@ 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 PropertiesTable from '.'
 
-const wrap = props => shallow(<PropertiesTable {...props} />)
+const wrap = props => new TW(shallow(<PropertiesTable onChange={() => { }} {...props} />), 'propertiesTable')
 
 let properties = [
-  { type: 'boolean', name: 'prop-1', value: true },
-  { type: 'boolean', name: 'prop-2', value: false },
+  { type: 'boolean', name: 'prop_1', label: 'Boolean', value: true },
+  { type: 'strict-boolean', name: 'prop_2', label: 'Strict Boolean', value: false },
+  { type: 'string', name: 'prop_3', label: 'String', value: 'value-3' },
+  { type: 'string', name: 'prop_3a', label: 'String', required: true, value: 'value-4' },
+  { type: 'string', enum: ['a', 'b', 'c'], name: 'prop_4', label: 'String enum', value: 'value-5' },
 ]
+const valueCallback = prop => {
+  const property = properties.find(p => p.name === prop.name)
+  return property ? property.value : null
+}
 
-it('renders boolean properties with correct labels', () => {
-  let wrapper = wrap({
-    properties,
-    valueCallback: prop => properties.find(p => p.name === prop.name).value,
+describe('PropertiesTable Component', () => {
+  it('renders all properties', () => {
+    const wrapper = wrap({ properties, valueCallback })
+    expect(wrapper.find('row-', true).length).toBe(properties.length)
+    expect(wrapper.find(`row-${properties[3].name}`).findText('header')).toBe('Prop 3a')
+  })
+
+  it('renders boolean properties', () => {
+    const wrapper = wrap({ properties, valueCallback })
+    expect(wrapper.find('switch-prop_1').prop('triState')).toBe(true)
+    expect(wrapper.find('switch-prop_1').prop('checked')).toBe(true)
+    expect(wrapper.find('switch-prop_2').prop('triState')).toBe(false)
+    expect(wrapper.find('switch-prop_2').prop('checked')).toBe(false)
+  })
+
+  it('renders string properties', () => {
+    const wrapper = wrap({ properties, valueCallback })
+    expect(wrapper.find('textInput-prop_3').prop('value')).toBe('value-3')
+    expect(wrapper.find('textInput-prop_3').prop('required')).toBe(false)
+    expect(wrapper.find('textInput-prop_3a').prop('value')).toBe('value-4')
+    expect(wrapper.find('textInput-prop_3a').prop('required')).toBe(true)
   })
-  expect(wrapper.children().length).toBe(2)
-  let item1 = wrapper.childAt(0)
-  let item2 = wrapper.childAt(1)
-  expect(item1.childAt(0).html().indexOf('Prop-1')).toBeGreaterThan(-1)
-  expect(item2.childAt(0).html().indexOf('Prop-2')).toBeGreaterThan(-1)
-})
 
-it('renders boolean properties with Switch components', () => {
-  let wrapper = wrap({
-    properties,
-    valueCallback: prop => properties.find(p => p.name === prop.name).value,
+  it('renders enum properties', () => {
+    const wrapper = wrap({ properties, valueCallback })
+    expect(wrapper.find('dropdown-prop_4').prop('items')[0].value).toBe(null)
+    expect(wrapper.find('dropdown-prop_4').prop('items')[2].value).toBe('b')
   })
-  expect(wrapper.children().length).toBe(2)
-  let item1 = wrapper.childAt(0)
-  let item2 = wrapper.childAt(1)
-  expect(item1.find('Switch').prop('checked')).toBe(true)
-  expect(item2.find('Switch').prop('checked')).toBe(false)
 })

+ 10 - 5
src/components/molecules/ScheduleItem/index.jsx

@@ -61,7 +61,7 @@ const Label = styled.div`
   line-height: 35px;
   margin-bottom: -8px;
 `
-const DropdownStyled = styled(Dropdown)`
+const DropdownStyled = styled(Dropdown) `
   font-size: 12px;
 `
 const ItemButton = props => css`
@@ -207,7 +207,7 @@ class ScheduleItem extends React.Component<Props> {
         useBold={this.shouldUseBold('month')}
         selectedItem={this.getFieldValue(items, 'month')}
         onChange={item => { this.handleMonthChange(item) }}
-        data-test-id="monthDropdown"
+        data-test-id="scheduleItem-monthDropdown"
       />
     )
   }
@@ -231,6 +231,7 @@ class ScheduleItem extends React.Component<Props> {
         useBold={this.shouldUseBold('dom')}
         selectedItem={this.getFieldValue(items, 'dom')}
         onChange={item => { this.props.onChange({ schedule: { dom: item.value } }) }}
+        data-test-id="scheduleItem-dayOfMonthDropdown"
       />
     )
   }
@@ -255,6 +256,7 @@ class ScheduleItem extends React.Component<Props> {
         useBold={this.shouldUseBold('dow')}
         selectedItem={this.getFieldValue(items, 'dow', true)}
         onChange={item => { this.props.onChange({ schedule: { dow: item.value } }) }}
+        data-test-id="scheduleItem-dayOfWeekDropdown"
       />
     )
   }
@@ -277,7 +279,7 @@ class ScheduleItem extends React.Component<Props> {
         useBold={this.shouldUseBold('hour')}
         selectedItem={this.getFieldValue(items, 'hour', true, 1)}
         onChange={item => { this.handleHourChange(item.value) }}
-        data-test-id="hourDropdown"
+        data-test-id="scheduleItem-hourDropdown"
       />
     )
   }
@@ -300,6 +302,7 @@ class ScheduleItem extends React.Component<Props> {
         useBold={this.shouldUseBold('minute')}
         selectedItem={this.getFieldValue(items, 'minute', true, 1)}
         onChange={item => { this.props.onChange({ schedule: { minute: item.value } }) }}
+        data-test-id="scheduleItem-minuteDropdown"
       />
     )
   }
@@ -336,6 +339,7 @@ class ScheduleItem extends React.Component<Props> {
             height={16}
             checked={enabled}
             onChange={enabled => { this.props.onChange({ enabled }, true) }}
+            data-test-id="scheduleItem-enabled"
           />
         </Data>
         <Data width={this.props.colWidths[1]}>
@@ -367,15 +371,16 @@ class ScheduleItem extends React.Component<Props> {
               letterSpacing: '1px',
               padding: '0 0 1px 3px',
             }}
+            data-test-id="scheduleItem-optionsButton"
           >•••</Button>
         </Data>
         <DeleteButton
-          data-test-id="deleteButton"
+          data-test-id="scheduleItem-deleteButton"
           onClick={this.props.onDeleteClick}
           hidden={this.props.item.enabled}
         />
         <SaveButton
-          data-test-id="saveButton"
+          data-test-id="scheduleItem-saveButton"
           onClick={this.props.onSaveSchedule}
           hidden={this.props.item.enabled || !this.props.unsavedSchedules.find(us => us.id === this.props.item.id)}
         />

+ 54 - 0
src/components/molecules/ScheduleItem/story.jsx

@@ -0,0 +1,54 @@
+/*
+Copyright (C) 2018  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 ScheduleItem from '.'
+
+const colWidths = ['6%', '18%', '10%', '18%', '10%', '10%', '23%', '5%']
+
+class Wrapper extends React.Component<{ enabled: boolean, shutdown_instances: boolean }> {
+  render() {
+    return (
+      <div style={{ width: '924px' }}>
+        <ScheduleItem
+          onChange={() => { }}
+          onDeleteClick={() => { }}
+          onSaveSchedule={() => { }}
+          onShowOptionsClick={() => { }}
+          unsavedSchedules={[]}
+          timezone="local"
+          colWidths={colWidths}
+          item={{
+            id: 'schedule-1',
+            enabled: this.props.enabled,
+            schedule: { hour: 1, minute: 1, dow: 2, dom: 3, month: 5 },
+            expiration_date: new Date(2018, 3, 25, 4, 0, 0),
+            shutdown_instances: this.props.shutdown_instances,
+          }}
+        />
+      </div >
+    )
+  }
+}
+
+storiesOf('ScheduleItem', module)
+  .add('default', () => (
+    <Wrapper enabled={false} shutdown_instances={false} />
+  ))
+  .add('enabled with shutdown_instances', () => (
+    <Wrapper enabled shutdown_instances />
+  ))

+ 62 - 0
src/components/molecules/ScheduleItem/test.jsx

@@ -0,0 +1,62 @@
+/*
+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 ScheduleItem from '.'
+
+const wrap = props => new TW(shallow(
+  // $FlowIgnore
+  (<ScheduleItem
+    {...props}
+    unsavedSchedules={[]}
+    colWidths={['6%', '18%', '10%', '18%', '10%', '10%', '23%', '5%']}
+  />)
+), 'scheduleItem')
+
+describe('ScheduleItem Component', () => {
+  it('should render all schedule properties', () => {
+    const wrapper = wrap({
+      item: {
+        id: 'schedule-1',
+        enabled: false,
+        schedule: { hour: 1, minute: 1, dow: 2, dom: 3, month: 5 },
+        expiration_date: new Date(2018, 3, 25, 4, 0, 0),
+        shutdown_instances: false,
+      },
+    })
+    expect(wrapper.find('enabled').prop('checked')).toBe(false)
+    expect(wrapper.find('hourDropdown').prop('selectedItem').value).toBe(1)
+    expect(wrapper.find('minuteDropdown').prop('selectedItem').value).toBe(1)
+    expect(wrapper.find('dayOfWeekDropdown').prop('selectedItem').value).toBe(2)
+    expect(wrapper.find('dayOfMonthDropdown').prop('selectedItem').value).toBe(3)
+    expect(wrapper.find('monthDropdown').prop('selectedItem').value).toBe(5)
+  })
+
+  it('should highlight options button if options are changed', () => {
+    const wrapper = wrap({
+      item: {
+        id: 'schedule-1',
+        enabled: false,
+        schedule: { hour: 1, minute: 1, dow: 2, dom: 3, month: 5 },
+        expiration_date: new Date(2018, 3, 25, 4, 0, 0),
+        shutdown_instances: true,
+      },
+    })
+    expect(wrapper.find('optionsButton').prop('hollow')).toBe(false)
+  })
+})

+ 4 - 3
src/components/molecules/SearchInput/index.jsx

@@ -42,12 +42,12 @@ const Wrapper = styled.div`
   width: ${props => props.open ? props.width : '50px'};
   ${props => props.open ? InputAnimation(props) : ''}
 `
-const SearchButtonStyled = styled(SearchButton)`
+const SearchButtonStyled = styled(SearchButton) `
   position: absolute;
   top: 8px;
   left: 8px;
 `
-const StatusIconStyled = styled(StatusIcon)`
+const StatusIconStyled = styled(StatusIcon) `
   position: absolute;
   right: 8px;
   top: 8px;
@@ -165,8 +165,9 @@ class SearchInput extends React.Component<Props, State> {
           }
           onClick={() => { this.handleSearchButtonClick() }}
           useFilterIcon={this.props.useFilterIcon}
+          data-test-id="searchInput-button"
         />
-        {this.props.loading ? <StatusIconStyled status="RUNNING" /> : null}
+        {this.props.loading ? <StatusIconStyled status="RUNNING" data-test-id="searchInput-loading" /> : null}
       </Wrapper>
     )
   }

+ 4 - 3
src/components/molecules/SearchInput/test.jsx

@@ -16,20 +16,21 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import React from 'react'
 import { shallow } from 'enzyme'
+import TW from '../../../utils/TestWrapper'
 import SearchInput from '.'
 
-const wrap = props => shallow(<SearchInput {...props} />)
+const wrap = props => new TW(shallow(<SearchInput {...props} />), 'searchInput')
 
 describe('SearchInput Component', () => {
   it('opens on button click', () => {
     let wrapper = wrap()
     expect(wrapper.prop('open')).toBe(false)
-    wrapper.find('Styled(SearchButton)').simulate('click')
+    wrapper.find('button').simulate('click')
     expect(wrapper.prop('open')).toBe(true)
   })
 
   it('has loading state', () => {
     let wrapper = wrap({ loading: true })
-    expect(wrapper.find('Styled(StatusIcon)').prop('status')).toBe('RUNNING')
+    expect(wrapper.find('loading').length).toBe(1)
   })
 })

+ 3 - 2
src/components/molecules/SideMenu/index.jsx

@@ -110,11 +110,12 @@ class SideMenu extends React.Component<Props, State> {
           open={this.state.open}
           onClick={() => { this.handleHamburgerClick() }}
           dangerouslySetInnerHTML={{ __html: hamburgerImage() }}
+          data-test-id="sideMenu-toggle"
         />
-        <Menu open={this.state.open}>
+        <Menu open={this.state.open} data-test-id="sideMenu-menu">
           {navigationMenu.filter(i => i.disabled ? !i.disabled : true).map(item => {
             return (
-              <MenuItem key={item.value} href={`/#/${item.value}`}>{item.label}</MenuItem>
+              <MenuItem key={item.value} href={`/#/${item.value}`} data-test-id={`sideMenu-item-${item.value}`}>{item.label}</MenuItem>
             )
           })}
         </Menu>

+ 15 - 10
src/components/molecules/SideMenu/test.jsx

@@ -12,20 +12,25 @@ 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 SideMenu from '.'
 
-const wrap = props => shallow(<SideMenu {...props} />)
+const wrap = props => new TW(shallow(<SideMenu {...props} />), 'sideMenu')
 
-it('opens menu on click', () => {
-  let wrapper = wrap()
-  expect(wrapper.childAt(1).prop('open')).toBe(false)
-  wrapper.childAt(0).simulate('click')
-  expect(wrapper.childAt(1).prop('open')).toBe(true)
-})
+describe('SideMenu Component', () => {
+  it('opens menu on click', () => {
+    let wrapper = wrap()
+    expect(wrapper.find('menu').prop('open')).toBe(false)
+    wrapper.find('toggle').simulate('click')
+    expect(wrapper.find('menu').prop('open')).toBe(true)
+  })
 
-it('renders at least one item in the list', () => {
-  let wrapper = wrap()
-  expect(wrapper.childAt(1).children().length).toBeGreaterThan(0)
+  it('renders at least one item in the list', () => {
+    let wrapper = wrap()
+    expect(wrapper.find('item-', true).length).toBeGreaterThan(0)
+  })
 })

+ 4 - 3
src/components/molecules/Table/index.jsx

@@ -123,6 +123,7 @@ class Table extends React.Component<Props> {
               width={this.props.columnsWidths && this.props.columnsWidths.length > 0 ? this.props.columnsWidths[i] : dataWidth}
               key={i}
               secondary={this.props.useSecondaryStyle}
+              data-test-id={`table-header-${i}`}
             >{headerItem}</HeaderData>
           )
         })}
@@ -135,7 +136,7 @@ class Table extends React.Component<Props> {
       return null
     }
 
-    return <NoItems secondary={this.props.useSecondaryStyle}>{this.props.noItemsLabel}</NoItems>
+    return <NoItems secondary={this.props.useSecondaryStyle} data-test-id="table-noItems">{this.props.noItemsLabel}</NoItems>
   }
 
   renderItems() {
@@ -148,7 +149,7 @@ class Table extends React.Component<Props> {
       <Body customStyle={this.props.bodyStyle}>
         {this.props.items.map((row, i) => {
           return (
-            <Row key={i} secondary={this.props.useSecondaryStyle}>
+            <Row key={i} secondary={this.props.useSecondaryStyle} data-test-id={`table-row-${i}`}>
               {
                 row.constructor === Array ? row.map((data, j) => {
                   let columnStyle = ''
@@ -158,7 +159,7 @@ class Table extends React.Component<Props> {
                   }
 
                   return (
-                    <RowData customStyle={columnStyle} width={dataWidth} key={`${i}-${j}`}>
+                    <RowData customStyle={columnStyle} width={dataWidth} key={`${i}-${j}`} data-test-id={`table-data-${i}-${j}`}>
                       {data}
                     </RowData>
                   )

+ 21 - 11
src/components/molecules/Table/test.jsx

@@ -12,11 +12,14 @@ 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 Table from '.'
 
-const wrap = props => shallow(<Table {...props} />)
+const wrap = props => new TW(shallow(<Table {...props} />), 'table')
 
 let items = [
   ['item-1', 'item-2', 'item-3', 'item-4', 'item-5'],
@@ -24,15 +27,22 @@ let items = [
 ]
 let headerItems = ['Header 1', 'Header 2', 'Header 3', 'Header 4', 'Header 5']
 
-it('renders header', () => {
-  let wrapper = wrap({ items, header: headerItems })
-  let header = wrapper.childAt(0)
-  expect(header.children().length).toBe(5)
-  expect(header.childAt(3).html().indexOf('Header 4')).toBeGreaterThan(-1)
-})
+describe('TTable Component', () => {
+  it('renders no items', () => {
+    let wrapper = wrap({ items: [], header: headerItems })
+    expect(wrapper.find('noItems').length).toBe(1)
+  })
+
+  it('renders header', () => {
+    let wrapper = wrap({ items, header: headerItems })
+    expect(wrapper.find('header-', true).length).toBe(headerItems.length)
+    headerItems.forEach((headerItem, i) => {
+      expect(wrapper.findText(`header-${i}`)).toBe(headerItem)
+    })
+  })
 
-it('renders header with calculated widths', () => {
-  let wrapper = wrap({ items, header: headerItems })
-  let header = wrapper.childAt(0)
-  expect(header.childAt(3).prop('width')).toBe('20%')
+  it('renders header with calculated widths', () => {
+    let wrapper = wrap({ items, header: headerItems })
+    expect(wrapper.find('header-', true).at(3).prop('width')).toBe('20%')
+  })
 })

+ 2 - 2
src/components/molecules/TaskItem/index.jsx

@@ -225,9 +225,9 @@ class TaskItem extends React.Component<Props> {
               <ProgressUpdateDate width={this.props.columnWidths[0]}>
                 <span>{DateUtils.getLocalTime(update.created_at).format('YYYY-MM-DD HH:mm:ss')}</span>
               </ProgressUpdateDate>
-              <ProgressUpdateValue>
+              <ProgressUpdateValue data-test-id={`taskItem-progressUpdateMessage-${i}`}>
                 {update.message}
-                {messageProgress && <ProgressBar style={{ margin: '8px 0' }} progress={Number(messageProgress)} />}
+                {messageProgress && <ProgressBar style={{ margin: '8px 0' }} progress={Number(messageProgress)} data-test-id={`taskItem-progressBar-${i}`} />}
               </ProgressUpdateValue>
             </ProgressUpdate>
           )

+ 16 - 10
src/components/molecules/TaskItem/test.jsx

@@ -12,11 +12,17 @@ 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 TaskItem from '.'
 
-const wrap = props => shallow(<TaskItem {...props} />)
+const wrap = props => new TW(shallow(
+  // $FlowIgnore
+  <TaskItem {...props} />
+), 'taskItem')
 
 let item = {
   progress_updates: [
@@ -32,14 +38,14 @@ let item = {
 }
 let columnWidths = ['26%', '18%', '36%', '20%']
 
-it('renders progress updates', () => {
-  let wrapper = wrap({ item, columnWidths, open: true })
-  let collapse = wrapper.find('Collapse')
-  expect(collapse.html().indexOf('the task has a progress of 50%')).toBeGreaterThan(-1)
-  expect(collapse.html().indexOf('the task is almost done')).toBeGreaterThan(-1)
-})
+describe('TaskItem Component', () => {
+  it('renders progress updates', () => {
+    let wrapper = wrap({ item, columnWidths, open: true })
+    expect(wrapper.findText('progressUpdateMessage-1')).toBe('the task is almost done')
+  })
 
-it('renders progress bar', () => {
-  let wrapper = wrap({ item, columnWidths, open: true })
-  expect(wrapper.find('ProgressBar').prop('progress')).toBe(50)
+  it('renders progress bar', () => {
+    let wrapper = wrap({ item, columnWidths, open: true })
+    expect(wrapper.find('progressBar-0').prop('progress')).toBe(50)
+  })
 })

+ 4 - 1
src/components/molecules/Timeline/index.jsx

@@ -164,9 +164,10 @@ class Timeline extends React.Component<Props> {
               key={item.id}
               innerRef={item => { this.itemRef = item }}
               onClick={() => { this.props.onItemClick(item) }}
+              data-test-id={`timeline-item-${item.id}`}
             >
               <StatusIcon status={item.status} useBackground />
-              <ItemLabel selected={this.props.selectedItem && this.props.selectedItem.id === item.id}>
+              <ItemLabel selected={this.props.selectedItem && this.props.selectedItem.id === item.id} data-test-id={`timeline-label-${item.id}`}>
                 {DateUtils.getLocalTime(item.created_at).format('DD MMM YYYY')}
               </ItemLabel>
             </Item>
@@ -184,6 +185,7 @@ class Timeline extends React.Component<Props> {
           forceShow={!this.props.items || !this.props.items.length}
           primary={Boolean(this.props.items && this.props.items.length)}
           onClick={this.props.onPreviousClick}
+          data-test-id="timeline-previous"
         />
         {this.renderMainLine()}
         {this.renderItems()}
@@ -191,6 +193,7 @@ class Timeline extends React.Component<Props> {
           orientation="right"
           forceShow={!this.props.items || !this.props.items.length}
           onClick={this.props.onNextClick}
+          data-test-id="timeline-next"
         />
       </Wrapper>
     )

+ 31 - 22
src/components/molecules/Timeline/test.jsx

@@ -12,12 +12,19 @@ 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 moment from 'moment'
+import TW from '../../../utils/TestWrapper'
 import Timeline from '.'
 
-const wrap = props => shallow(<Timeline {...props} />)
+const wrap = props => new TW(shallow(
+  // $FlowIgnore
+  <Timeline {...props} />
+), 'timeline')
 
 let items = [
   { id: 'item-1', status: 'ERROR', created_at: new Date(2017, 1, 2) },
@@ -25,27 +32,29 @@ let items = [
   { id: 'item-3', status: 'RUNNING', created_at: new Date(2017, 3, 4) },
 ]
 
-it('renders with correct dates', () => {
-  let wrapper = wrap({ items, selectedItem: items[2] })
-  let itemsWrapper = wrapper.childAt(2).childAt(0)
-  expect(itemsWrapper.childAt(0).html().indexOf('02 Feb 2017')).toBeGreaterThan(-1)
-  expect(itemsWrapper.childAt(1).html().indexOf('03 Mar 2017')).toBeGreaterThan(-1)
-  expect(itemsWrapper.childAt(2).html().indexOf('04 Apr 2017')).toBeGreaterThan(-1)
-})
+describe('Timeline Component', () => {
+  it('renders with correct dates', () => {
+    let wrapper = wrap({ items, selectedItem: items[2] })
+    expect(wrapper.find('label-', true).length).toBe(items.length)
+    items.forEach(item => {
+      expect(wrapper.findText(`label-${item.id}`)).toBe(moment(item.created_at).format('DD MMM YYYY'))
+    })
+  })
 
-it('dispatches item click', () => {
-  let onItemClick = sinon.spy()
-  let wrapper = wrap({ items, selectedItem: items[2], onItemClick })
-  wrapper.childAt(2).childAt(0).childAt(1).simulate('click')
-  expect(onItemClick.args[0][0].id).toBe('item-2')
-})
+  it('dispatches item click', () => {
+    let onItemClick = sinon.spy()
+    let wrapper = wrap({ items, selectedItem: items[2], onItemClick })
+    wrapper.find(`item-${items[1].id}`).simulate('click')
+    expect(onItemClick.args[0][0].id).toBe('item-2')
+  })
 
-it('dispatches next and previous click', () => {
-  let onPreviousClick = sinon.spy()
-  let onNextClick = sinon.spy()
-  let wrapper = wrap({ items, selectedItem: items[2], onPreviousClick, onNextClick })
-  wrapper.find('Styled(Arrow)').at(0).simulate('click')
-  wrapper.find('Styled(Arrow)').at(1).simulate('click')
-  expect(onPreviousClick.calledOnce).toBe(true)
-  expect(onNextClick.calledOnce).toBe(true)
+  it('dispatches next and previous click', () => {
+    let onPreviousClick = sinon.spy()
+    let onNextClick = sinon.spy()
+    let wrapper = wrap({ items, selectedItem: items[2], onPreviousClick, onNextClick })
+    wrapper.find('previous').simulate('click')
+    wrapper.find('next').simulate('click')
+    expect(onPreviousClick.calledOnce).toBe(true)
+    expect(onNextClick.calledOnce).toBe(true)
+  })
 })

+ 3 - 2
src/components/molecules/UserDropdown/index.jsx

@@ -162,7 +162,7 @@ class UserDropdown extends React.Component<Props, State> {
     }
     return (
       <ListHeader>
-        <Username>{this.props.user.name}</Username>
+        <Username data-test-id="userDropdown-username">{this.props.user.name}</Username>
         <Email>{this.props.user.email}</Email>
       </ListHeader>
     )
@@ -190,7 +190,7 @@ class UserDropdown extends React.Component<Props, State> {
               onMouseDown={() => { this.itemMouseDown = true }}
               onMouseUp={() => { this.itemMouseDown = false }}
             >
-              <Label selectable onClick={() => { this.handleItemClick(item) }}>{item.label}</Label>
+              <Label selectable onClick={() => { this.handleItemClick(item) }} data-test-id={`userDropdown-label-${item.value}`}>{item.label}</Label>
             </ListItem>
           )
         }) : null}
@@ -207,6 +207,7 @@ class UserDropdown extends React.Component<Props, State> {
           onMouseUp={() => { this.itemMouseDown = false }}
           onClick={() => this.handleButtonClick()}
           white={this.props.white}
+          data-test-id="userDropdown-button"
         />
         {this.renderList()}
       </Wrapper>

+ 26 - 19
src/components/molecules/UserDropdown/test.jsx

@@ -12,33 +12,40 @@ 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 UserDropdown from '.'
 
-const wrap = props => shallow(<UserDropdown {...props} />)
+const wrap = props => new TW(shallow(
+  // $FlowIgnore
+  <UserDropdown {...props} />
+), 'userDropdown')
 
 let user = { name: 'User name', email: 'email@email.com' }
 
-it('opens dropdown on click', () => {
-  let wrapper = wrap({ user })
-  wrapper.childAt(0).simulate('click')
-  expect(wrapper.childAt(1).children().length).toBe(2)
-})
+describe('UserDropdown Component', () => {
+  it('opens dropdown on click', () => {
+    let wrapper = wrap({ user })
+    expect(wrapper.find('username').length).toBe(0)
+    wrapper.find('button').simulate('click')
+    expect(wrapper.find('username').length).toBe(1)
+  })
 
-it('renders user info', () => {
-  let wrapper = wrap({ user })
-  wrapper.childAt(0).simulate('click')
-  expect(wrapper.childAt(1).html().indexOf('User name')).toBeGreaterThan(-1)
-  expect(wrapper.childAt(1).html().indexOf('email@email.com')).toBeGreaterThan(-1)
-})
+  it('renders user info', () => {
+    let wrapper = wrap({ user })
+    wrapper.find('button').simulate('click')
+    expect(wrapper.findText('username')).toBe(user.name)
+  })
 
-it('dispatches item click', () => {
-  let onItemClick = sinon.spy()
-  let wrapper = wrap({ user, onItemClick })
-  wrapper.childAt(0).simulate('click')
-  let signout = wrapper.findWhere(w => w.prop('onClick') && w.html().indexOf('Sign Out') > -1)
-  signout.simulate('click')
-  expect(onItemClick.args[0][0].value).toBe('signout')
+  it('dispatches item click', () => {
+    let onItemClick = sinon.spy()
+    let wrapper = wrap({ user, onItemClick })
+    wrapper.find('button').simulate('click')
+    wrapper.find('label-signout').simulate('click')
+    expect(onItemClick.args[0][0].value).toBe('signout')
+  })
 })

+ 2 - 2
src/components/molecules/WizardBreadcrumbs/index.jsx

@@ -27,7 +27,7 @@ const Wrapper = styled.div`
   display: flex;
   justify-content: center;
 `
-const ArrowStyled = styled(Arrow)``
+const ArrowStyled = styled(Arrow) ``
 const Breadcrumb = styled.div`
   display: flex;
   align-items: center;
@@ -54,7 +54,7 @@ class WizardBreadcrumbs extends React.Component<Props> {
         {pages.map(page => {
           return (
             <Breadcrumb key={page.id}>
-              <Name selected={this.props.selected.id === page.id}>{page.breadcrumb}</Name>
+              <Name selected={this.props.selected.id === page.id} data-test-id={`wBreadCrumbs-name-${page.id}`}>{page.breadcrumb}</Name>
               <ArrowStyled primary={this.props.selected.id === page.id} useDefaultCursor />
             </Breadcrumb>
           )

+ 20 - 15
src/components/molecules/WizardBreadcrumbs/test.jsx

@@ -12,27 +12,32 @@ 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 WizardBreadcrumbs from '.'
+import TW from '../../../utils/TestWrapper'
 import { wizardConfig } from '../../../config'
 
-const wrap = props => shallow(<WizardBreadcrumbs {...props} />)
+const wrap = props => new TW(shallow(<WizardBreadcrumbs {...props} />), 'wBreadCrumbs')
 
-it('renders correct number of crumbs for replica', () => {
-  let wrapper = wrap({ selected: wizardConfig.pages[2], wizardType: 'replica' })
-  let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== 'replica')
-  expect(wrapper.children().length).toBe(pages.length)
-})
+describe('WizardBreadcrumbs Component', () => {
+  it('renders correct number of crumbs for replica', () => {
+    let wrapper = wrap({ selected: wizardConfig.pages[2], wizardType: 'replica' })
+    let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== 'replica')
+    expect(wrapper.find('name-', true).length).toBe(pages.length)
+  })
 
-it('renders correct number of crumbs for migration', () => {
-  let wrapper = wrap({ selected: wizardConfig.pages[2], wizardType: 'migration' })
-  let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== 'migration')
-  expect(wrapper.children().length).toBe(pages.length)
-})
+  it('renders correct number of crumbs for migration', () => {
+    let wrapper = wrap({ selected: wizardConfig.pages[2], wizardType: 'migration' })
+    let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== 'migration')
+    expect(wrapper.find('name-', true).length).toBe(pages.length)
+  })
 
-it('has correct page selected', () => {
-  let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== 'migration')
-  let wrapper = wrap({ selected: pages[2], wizardType: 'migration' })
-  expect(wrapper.findWhere(w => w.prop('selected')).html().indexOf(pages[2].breadcrumb)).toBeGreaterThan(-1)
+  it('has correct page selected', () => {
+    let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== 'migration')
+    let wrapper = wrap({ selected: pages[2], wizardType: 'migration' })
+    expect(wrapper.findText(`name-${pages[2].id}`)).toBe(pages[2].breadcrumb)
+  })
 })

Некоторые файлы не были показаны из-за большого количества измененных файлов