Explorar el Código

Add unit tests

Sergiu Miclea hace 4 años
padre
commit
4a7432971b
Se han modificado 100 ficheros con 2510 adiciones y 1588 borrados
  1. 1 1
      .storybook/preview.js
  2. 11 3
      jest.config.ts
  3. 10 6
      package.json
  4. 13 2
      src/components/App.tsx
  5. 99 0
      src/components/modules/DashboardModule/DashboardActivity/DashboardActivity.spec.tsx
  6. 4 4
      src/components/modules/DashboardModule/DashboardActivity/DashboardActivity.tsx
  7. 85 0
      src/components/modules/DashboardModule/DashboardBarChart/DashboardBarChart.spec.tsx
  8. 2 4
      src/components/modules/DashboardModule/DashboardBarChart/DashboardBarChart.tsx
  9. 1 1
      src/components/modules/DashboardModule/DashboardExecutions/DashboardExecutions.tsx
  10. 1 2
      src/components/modules/TransferModule/TransferItemModal/TransferItemModal.tsx
  11. 2 2
      src/components/modules/TransferModule/TransferListItem/TransferListItem.tsx
  12. 0 0
      src/components/modules/TransferModule/TransferListItem/images/arrow.svg
  13. 0 0
      src/components/modules/TransferModule/TransferListItem/images/schedule.svg
  14. 6 0
      src/components/modules/TransferModule/TransferListItem/package.json
  15. 3 3
      src/components/modules/TransferModule/TransferListItem/story.tsx
  16. 0 0
      src/components/modules/TransferModule/TransferListItem/test.tsx
  17. 1 1
      src/components/smart/AssessmentsPage/AssessmentsPage.tsx
  18. 1 1
      src/components/smart/DashboardPage/DashboardPage.tsx
  19. 1 1
      src/components/smart/EndpointsPage/EndpointsPage.tsx
  20. 1 1
      src/components/smart/LogsPage/LogsPage.tsx
  21. 3 3
      src/components/smart/MigrationsPage/MigrationsPage.tsx
  22. 1 1
      src/components/smart/MinionPoolsPage/MinionPoolsPage.tsx
  23. 0 0
      src/components/smart/PageHeader/PageHeader.tsx
  24. 0 0
      src/components/smart/PageHeader/package.json
  25. 0 0
      src/components/smart/PageHeader/story.tsx
  26. 1 1
      src/components/smart/ProjectsPage/ProjectsPage.tsx
  27. 3 3
      src/components/smart/ReplicasPage/ReplicasPage.tsx
  28. 1 1
      src/components/smart/UsersPage/UsersPage.tsx
  29. 71 0
      src/components/ui/AlertModal/AlertModal.spec.tsx
  30. 0 56
      src/components/ui/AlertModal/test.tsx
  31. 46 0
      src/components/ui/Arrow/Arrow.spec.tsx
  32. 36 0
      src/components/ui/AutocompleteInput/AutocompleteInput.spec.tsx
  33. 37 0
      src/components/ui/Button/Button.spec.tsx
  34. 0 43
      src/components/ui/Button/test.tsx
  35. 39 0
      src/components/ui/Checkbox/Checkbox.spec.tsx
  36. 0 44
      src/components/ui/Checkbox/test.tsx
  37. 14 15
      src/components/ui/CopyButton/CopyButton.spec.tsx
  38. 38 0
      src/components/ui/CopyMultilineValue/CopyMultilineValue.spec.tsx
  39. 0 38
      src/components/ui/CopyMultilineValue/test.tsx
  40. 34 0
      src/components/ui/CopyValue/CopyValue.spec.tsx
  41. 0 39
      src/components/ui/CopyValue/test.tsx
  42. 61 0
      src/components/ui/DatetimePicker/DatetimePicker.spec.tsx
  43. 0 42
      src/components/ui/DatetimePicker/test.tsx
  44. 94 0
      src/components/ui/Dropdowns/ActionDropdown/ActionDropdown.spec.tsx
  45. 0 93
      src/components/ui/Dropdowns/ActionDropdown/test.tsx
  46. 65 0
      src/components/ui/Dropdowns/AutocompleteDropdown/AutocompleteDropdown.spec.tsx
  47. 65 0
      src/components/ui/Dropdowns/Dropdown/Dropdown.spec.tsx
  48. 0 53
      src/components/ui/Dropdowns/Dropdown/test.tsx
  49. 42 0
      src/components/ui/Dropdowns/DropdownButton/DropdownButton.spec.tsx
  50. 0 45
      src/components/ui/Dropdowns/DropdownButton/test.tsx
  51. 49 0
      src/components/ui/Dropdowns/DropdownFilter/DropdownFilter.spec.tsx
  52. 0 37
      src/components/ui/Dropdowns/DropdownFilter/test.tsx
  53. 49 0
      src/components/ui/Dropdowns/DropdownFilterGroup/DropdownFilterGroup.spec.tsx
  54. 0 48
      src/components/ui/Dropdowns/DropdownFilterGroup/test.tsx
  55. 74 0
      src/components/ui/Dropdowns/DropdownInput/DropdownInput.spec.tsx
  56. 0 72
      src/components/ui/Dropdowns/DropdownInput/test.tsx
  57. 79 0
      src/components/ui/Dropdowns/DropdownLink/DropdownLink.spec.tsx
  58. 2 2
      src/components/ui/Dropdowns/DropdownLink/DropdownLink.tsx
  59. 0 40
      src/components/ui/Dropdowns/DropdownLink/test.tsx
  60. 47 0
      src/components/ui/Dropdowns/NewItemDropdown/NewItemDropdown.spec.tsx
  61. 0 41
      src/components/ui/Dropdowns/NewItemDropdown/test.tsx
  62. 89 0
      src/components/ui/Dropdowns/NotificationDropdown/NotificationDropdown.spec.tsx
  63. 0 99
      src/components/ui/Dropdowns/NotificationDropdown/test.tsx
  64. 57 0
      src/components/ui/Dropdowns/UserDropdown/UserDropdown.spec.tsx
  65. 0 52
      src/components/ui/Dropdowns/UserDropdown/test.tsx
  66. 175 0
      src/components/ui/FieldInput/FieldInput.spec.tsx
  67. 33 0
      src/components/ui/FileInput/FileInput.spec.tsx
  68. 1 1
      src/components/ui/FileInput/FileInput.tsx
  69. 7 12
      src/components/ui/HorizontalLoading/HorizontalLoading.spec.tsx
  70. 33 0
      src/components/ui/InfoIcon/InfoIcon.spec.tsx
  71. 137 0
      src/components/ui/Lists/FilterList/FilterList.spec.tsx
  72. 9 9
      src/components/ui/Lists/FilterList/FilterList.tsx
  73. 0 75
      src/components/ui/Lists/FilterList/test.tsx
  74. 110 0
      src/components/ui/Lists/MainList/MainList.spec.tsx
  75. 0 90
      src/components/ui/Lists/MainList/test.tsx
  76. 131 0
      src/components/ui/Lists/MainListFilter/MainListFilter.spec.tsx
  77. 0 64
      src/components/ui/Lists/MainListFilter/test.tsx
  78. 0 7
      src/components/ui/Lists/MainListItem/package.json
  79. 34 0
      src/components/ui/LoadingButton/LoadingButton.spec.tsx
  80. 0 29
      src/components/ui/LoadingButton/test.tsx
  81. 13 17
      src/components/ui/Logo/Logo.spec.tsx
  82. 33 0
      src/components/ui/Modal/Modal.spec.tsx
  83. 0 38
      src/components/ui/Modal/test.tsx
  84. 90 0
      src/components/ui/Pagination/Pagination.spec.tsx
  85. 2 2
      src/components/ui/Pagination/Pagination.tsx
  86. 81 0
      src/components/ui/Panel/Panel.spec.tsx
  87. 2 4
      src/components/ui/Panel/Panel.tsx
  88. 0 91
      src/components/ui/Panel/test.tsx
  89. 12 15
      src/components/ui/PasswordValue/PasswordValue.spec.tsx
  90. 12 18
      src/components/ui/ProgressBar/ProgressBar.spec.tsx
  91. 0 30
      src/components/ui/ProgressBar/test.tsx
  92. 113 0
      src/components/ui/PropertiesTable/PropertiesTable.spec.tsx
  93. 0 66
      src/components/ui/PropertiesTable/test.tsx
  94. 44 0
      src/components/ui/RadioInput/RadioInput.spec.tsx
  95. 0 30
      src/components/ui/RadioInput/test.tsx
  96. 34 0
      src/components/ui/ReloadButton/ReloadButton.spec.tsx
  97. 0 32
      src/components/ui/ReloadButton/test.tsx
  98. 8 11
      src/components/ui/SearchButton/SearchButton.spec.tsx
  99. 0 47
      src/components/ui/SearchButton/test.tsx
  100. 57 0
      src/components/ui/SearchInput/SearchInput.spec.tsx

+ 1 - 1
.storybook/preview.js

@@ -3,7 +3,7 @@ import { addDecorator } from '@storybook/react'
 import styled, { createGlobalStyle } from 'styled-components'
 import styled, { createGlobalStyle } from 'styled-components'
 
 
 import { ThemePalette, ThemeProps } from '@src/components/Theme'
 import { ThemePalette, ThemeProps } from '@src/components/Theme'
-import Fonts from '../src/components/atoms/Fonts'
+import Fonts from '@src/components/ui/Fonts'
 import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
 import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
 
 
 const Wrapper = styled.div`
 const Wrapper = styled.div`

+ 11 - 3
jest.config.ts

@@ -82,7 +82,10 @@ export default {
   // ],
   // ],
 
 
   // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
   // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
-  // moduleNameMapper: {},
+  moduleNameMapper: {
+    '@src/(.*)': '<rootDir>/src/$1',
+    '@tests/(.*)': '<rootDir>/tests/$1',
+  },
 
 
   // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
   // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
   // modulePathIgnorePatterns: [],
   // modulePathIgnorePatterns: [],
@@ -126,7 +129,9 @@ export default {
   // runner: "jest-runner",
   // runner: "jest-runner",
 
 
   // The paths to modules that run some code to configure or set up the testing environment before each test
   // The paths to modules that run some code to configure or set up the testing environment before each test
-  // setupFiles: [],
+  setupFiles: [
+    '<rootDir>/tests/setup.js',
+  ],
 
 
   // A list of paths to modules that run some code to configure or set up the testing framework before each test
   // A list of paths to modules that run some code to configure or set up the testing framework before each test
   // setupFilesAfterEnv: [],
   // setupFilesAfterEnv: [],
@@ -172,7 +177,10 @@ export default {
   // timers: "real",
   // timers: "real",
 
 
   // A map from regular expressions to paths to transformers
   // A map from regular expressions to paths to transformers
-  // transform: undefined,
+  transform: {
+    '\\.[jt]sx?$': 'babel-jest',
+    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/tests/fileTransform.js',
+  },
 
 
   // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
   // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
   // transformIgnorePatterns: [
   // transformIgnorePatterns: [

+ 10 - 6
package.json

@@ -14,12 +14,15 @@
     "tsc": "npx tsc --skipLibCheck",
     "tsc": "npx tsc --skipLibCheck",
     "eslint": "npx eslint \"src/**\" \"server/**\"",
     "eslint": "npx eslint \"src/**\" \"server/**\"",
     "test": "jest",
     "test": "jest",
-    "test-release": "node ./server/testRelease",
+    "test-release": "node ./tests/testRelease",
+    "test-coverage": "node ./tests/testCoverage",
     "storybook": "start-storybook"
     "storybook": "start-storybook"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@storybook/react": "^6.4.13",
     "@storybook/react": "^6.4.13",
+    "@testing-library/dom": "^8.11.1",
     "@testing-library/react": "^12.1.2",
     "@testing-library/react": "^12.1.2",
+    "@testing-library/user-event": "^13.5.0",
     "@types/connect": "^3.4.33",
     "@types/connect": "^3.4.33",
     "@types/express": "^4.17.6",
     "@types/express": "^4.17.6",
     "@types/file-saver": "^2.0.1",
     "@types/file-saver": "^2.0.1",
@@ -30,11 +33,12 @@
     "@types/react-dom": "^16.9.8",
     "@types/react-dom": "^16.9.8",
     "@types/react-modal": "^3.10.5",
     "@types/react-modal": "^3.10.5",
     "@types/react-notification-system": "^0.2.39",
     "@types/react-notification-system": "^0.2.39",
-    "@types/react-router-dom": "^5.1.5",
+    "@types/react-router-dom": "^5.1.2",
     "@types/react-tooltip": "^4.2.4",
     "@types/react-tooltip": "^4.2.4",
     "@types/styled-components": "^5.1.0",
     "@types/styled-components": "^5.1.0",
     "@typescript-eslint/eslint-plugin": "^5.6.0",
     "@typescript-eslint/eslint-plugin": "^5.6.0",
     "@typescript-eslint/parser": "^5.6.0",
     "@typescript-eslint/parser": "^5.6.0",
+    "cross-spawn": "^7.0.3",
     "cypress": "^3.2.0",
     "cypress": "^3.2.0",
     "eslint": "^8.2.0",
     "eslint": "^8.2.0",
     "eslint-config-airbnb": "19.0.0",
     "eslint-config-airbnb": "19.0.0",
@@ -45,8 +49,7 @@
     "eslint-plugin-react": "^7.27.0",
     "eslint-plugin-react": "^7.27.0",
     "eslint-plugin-react-hooks": "^4.3.0",
     "eslint-plugin-react-hooks": "^4.3.0",
     "jest": "^27.3.1",
     "jest": "^27.3.1",
-    "nodemon": "^2.0.4",
-    "ts-node": "^10.4.0"
+    "nodemon": "^2.0.4"
   },
   },
   "dependencies": {
   "dependencies": {
     "@babel/core": "^7.7.2",
     "@babel/core": "^7.7.2",
@@ -84,7 +87,7 @@
     "path": "^0.12.7",
     "path": "^0.12.7",
     "react": "^16.13.1",
     "react": "^16.13.1",
     "react-collapse": "^5.0.1",
     "react-collapse": "^5.0.1",
-    "react-datetime": "^2.10.3",
+    "react-datetime": "^3.1.1",
     "react-dom": "^16.13.1",
     "react-dom": "^16.13.1",
     "react-hot-loader": "^4.12.17",
     "react-hot-loader": "^4.12.17",
     "react-modal": "^3.11.2",
     "react-modal": "^3.11.2",
@@ -98,6 +101,7 @@
     "rimraf": "^2.6.2",
     "rimraf": "^2.6.2",
     "styled-components": "^4.4.1",
     "styled-components": "^4.4.1",
     "tai-password-strength": "^1.1.2",
     "tai-password-strength": "^1.1.2",
+    "ts-node": "10.4.0",
     "typescript": "^4.5.3",
     "typescript": "^4.5.3",
     "url-loader": "^4.1.0",
     "url-loader": "^4.1.0",
     "webpack": "^4.41.2",
     "webpack": "^4.41.2",
@@ -125,6 +129,6 @@
     "ua-parser-js": "^0.7.24",
     "ua-parser-js": "^0.7.24",
     "underscore": "^1.12.1",
     "underscore": "^1.12.1",
     "y18n": "^3.2.2",
     "y18n": "^3.2.2",
-    "yargs-parser": "^13.1.2"
+    "yargs-parser": "^20.2.9"
   }
   }
 }
 }

+ 13 - 2
src/components/App.tsx

@@ -120,6 +120,7 @@ class App extends React.Component<{}, State> {
     }) => (
     }) => (
       <Route
       <Route
         path={options.path}
         path={options.path}
+        // @ts-ignore
         exact={options.exact}
         exact={options.exact}
         render={() => (
         render={() => (
           <MessagePage
           <MessagePage
@@ -142,6 +143,7 @@ class App extends React.Component<{}, State> {
           showAuthAnimation: true,
           showAuthAnimation: true,
         })
         })
       }
       }
+      // @ts-ignore
       return <Route path={path} component={component} exact={exact} />
       return <Route path={path} component={component} exact={exact} />
     }
     }
 
 
@@ -173,6 +175,7 @@ class App extends React.Component<{}, State> {
         })
         })
       }
       }
       if (userStore.loggedUser?.isAdmin) {
       if (userStore.loggedUser?.isAdmin) {
+        // @ts-ignore
         return <Route path={actualPath} exact={exact} component={component} />
         return <Route path={actualPath} exact={exact} component={component} />
       }
       }
       return null
       return null
@@ -184,9 +187,14 @@ class App extends React.Component<{}, State> {
         <Router>
         <Router>
           <Switch>
           <Switch>
             {configLoader.isFirstLaunch ? (
             {configLoader.isFirstLaunch ? (
+            // @ts-ignore
               <Route path="/" component={SetupPage} exact />
               <Route path="/" component={SetupPage} exact />
+            // @ts-ignore
             ) : renderRoute('/', DashboardPage, true)}
             ) : renderRoute('/', DashboardPage, true)}
-            <Route path="/login" component={LoginPage} />
+            {
+              // @ts-ignore
+              <Route path="/login" component={LoginPage} />
+            }
             {renderRoute('/dashboard', DashboardPage)}
             {renderRoute('/dashboard', DashboardPage)}
             {renderRoute('/replicas', ReplicasPage, true)}
             {renderRoute('/replicas', ReplicasPage, true)}
             {renderRoute('/replicas/:id', ReplicaDetailsPage, true)}
             {renderRoute('/replicas/:id', ReplicaDetailsPage, true)}
@@ -208,7 +216,10 @@ class App extends React.Component<{}, State> {
             {renderOptionalRoute('projects', ProjectDetailsPage, '/projects/:id')}
             {renderOptionalRoute('projects', ProjectDetailsPage, '/projects/:id')}
             {renderOptionalRoute('logging', LogsPage)}
             {renderOptionalRoute('logging', LogsPage)}
             {renderRoute('/streamlog', LogStreamPage)}
             {renderRoute('/streamlog', LogStreamPage)}
-            <Route component={MessagePage} />
+            {
+              // @ts-ignore
+              <Route component={MessagePage} />
+            }
           </Switch>
           </Switch>
         </Router>
         </Router>
         <NotificationsModule />
         <NotificationsModule />

+ 99 - 0
src/components/modules/DashboardModule/DashboardActivity/DashboardActivity.spec.tsx

@@ -0,0 +1,99 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import { NotificationItemData } from '@src/@types/NotificationItem'
+import progressImage from '@src/components/ui/StatusComponents/StatusIcon/images/progress'
+import { ThemePalette } from '@src/components/Theme'
+import DashboardActivity from '.'
+
+const encodedProgressImage = encodeURIComponent(progressImage(ThemePalette.grayscale[3], ThemePalette.primary))
+
+jest.mock('react-router-dom', () => ({ Link: 'a' }))
+
+const ITEMS: NotificationItemData[] = [
+  {
+    id: '1',
+    type: 'replica',
+    status: 'ERROR',
+    name: 'Replica 1',
+    description: 'Replica 1 description',
+  },
+  {
+    id: '2',
+    type: 'migration',
+    status: 'RUNNING',
+    name: 'Migration 1',
+    description: 'Migration 1 description',
+  },
+  {
+    id: '3',
+    type: 'migration',
+    status: 'COMPLETED',
+    name: 'Migration 2',
+    description: 'Migration 2 description',
+  },
+]
+
+describe('DashboardActivity', () => {
+  it('renders no recent activity', () => {
+    render(<DashboardActivity notificationItems={[]} />)
+    expect(TestUtils.select('DashboardActivity__Message')!.textContent).toContain('There is no recent activity')
+  })
+
+  it('fires new click', () => {
+    const onNewClick = jest.fn()
+    render(<DashboardActivity notificationItems={[]} onNewClick={onNewClick} />)
+    TestUtils.select('Button__StyledButton')!.click()
+    expect(onNewClick).toHaveBeenCalled()
+  })
+
+  it('renders loading', () => {
+    const { rerender } = render(<DashboardActivity notificationItems={[]} loading />)
+    expect(TestUtils.select('DashboardActivity__LoadingWrapper')).toBeTruthy()
+
+    rerender(<DashboardActivity notificationItems={[]} />)
+    expect(TestUtils.select('DashboardActivity__LoadingWrapper')).toBeFalsy()
+  })
+
+  it('renders all items', () => {
+    render(<DashboardActivity notificationItems={ITEMS} />)
+
+    const listItemsEl = TestUtils.selectAll('DashboardActivity__ListItem')
+    expect(listItemsEl.length).toBe(ITEMS.length)
+  })
+
+  it.each`
+    idx  | href                     | expectedStatusIcon
+    ${0} | ${'/replicas/1'}         | ${'error-hollow.svg'}
+    ${1} | ${'/migrations/2/tasks'} | ${encodedProgressImage}
+    ${2} | ${'/migrations/3'}       | ${'success-hollow.svg'}
+  `('renders item with href $href', ({
+    idx, href, expectedStatusIcon,
+  }) => {
+    render(<DashboardActivity notificationItems={ITEMS} />)
+
+    const itemElement = TestUtils.selectAll('DashboardActivity__ListItem')[idx]
+    expect(itemElement.getAttribute('to')).toBe(href)
+
+    const background = window.getComputedStyle(TestUtils.select('StatusIcon__Wrapper', itemElement)!).background
+    expect(background).toContain(expectedStatusIcon)
+
+    expect(TestUtils.select('NotificationDropdown__ItemReplicaBadge', itemElement)!.textContent).toContain(ITEMS[idx].type === 'replica' ? 'RE' : 'MI')
+    expect(TestUtils.select('NotificationDropdown__ItemTitle', itemElement)!.textContent).toContain(ITEMS[idx].name)
+    expect(TestUtils.select('NotificationDropdown__ItemDescription', itemElement)!.textContent).toContain(ITEMS[idx].description)
+  })
+})

+ 4 - 4
src/components/modules/DashboardModule/DashboardActivity/DashboardActivity.tsx

@@ -87,10 +87,10 @@ const Message = styled.div<any>`
 
 
 type Props = {
 type Props = {
   notificationItems: NotificationItemData[],
   notificationItems: NotificationItemData[],
-  style: any,
-  loading: boolean,
-  large: boolean,
-  onNewClick: () => void,
+  style?: React.CSSProperties | null,
+  loading?: boolean,
+  large?: boolean,
+  onNewClick?: () => void,
 }
 }
 @observer
 @observer
 class DashboardActivity extends React.Component<Props> {
 class DashboardActivity extends React.Component<Props> {

+ 85 - 0
src/components/modules/DashboardModule/DashboardBarChart/DashboardBarChart.spec.tsx

@@ -0,0 +1,85 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import { ThemePalette } from '@src/components/Theme'
+import userEvent from '@testing-library/user-event'
+import DashboardBarChart from '.'
+
+const DATA: DashboardBarChart['props']['data'] = [
+  {
+    label: 'label 1',
+    values: [10, 15],
+    data: 'data 1',
+  },
+  {
+    label: 'label 2',
+    values: [20, 25],
+    data: 'data 2',
+  },
+]
+
+describe('DashboardBarChart', () => {
+  it('renders all data correctly', () => {
+    render(<DashboardBarChart data={DATA} yNumTicks={3} />)
+
+    // Y ticks
+
+    const yTickEl = TestUtils.selectAll('DashboardBarChart__YTick')
+    expect(yTickEl.length).toBe(3)
+    expect(yTickEl[0].textContent).toBe('0')
+    expect(yTickEl[1].textContent).toBe('20')
+    expect(yTickEl[2].textContent).toBe('40')
+
+    // Bars
+
+    const barsEl = TestUtils.selectAll('DashboardBarChart__Bar-')
+    expect(barsEl.length).toBe(DATA.length)
+    expect(barsEl[0].textContent).toBe('label 1')
+    expect(barsEl[1].textContent).toBe('label 2')
+  })
+
+  it.each`
+    barIndex | stackedBarIndex | expectedHeight                    | expectedColor
+    ${0}     | ${0}            | ${(DATA[0].values[1] / 45) * 100} | ${ThemePalette.alert}
+    ${0}     | ${1}            | ${(DATA[0].values[0] / 45) * 100} | ${ThemePalette.primary}
+    ${1}     | ${0}            | ${(DATA[1].values[1] / 45) * 100} | ${ThemePalette.alert}
+    ${1}     | ${1}            | ${(DATA[1].values[0] / 45) * 100} | ${ThemePalette.primary}
+  `('renders bar index $barIndex, stacked bar index $stackedBarIndex with height $expectedHeight and color $expectedColor', ({
+    barIndex, stackedBarIndex, expectedHeight, expectedColor,
+  }) => {
+    render(<DashboardBarChart data={DATA} yNumTicks={3} colors={[ThemePalette.alert, ThemePalette.primary]} />)
+
+    const stackedBarEl = TestUtils.selectAll('DashboardBarChart__StackedBar-', TestUtils.selectAll('DashboardBarChart__Bar-')[barIndex])[stackedBarIndex]
+    const style = window.getComputedStyle(stackedBarEl)
+
+    expect(parseFloat(style.height)).toBeCloseTo(expectedHeight)
+    expect(TestUtils.rgbToHex(style.background)).toBe(expectedColor)
+  })
+
+  it.each`
+  barIndex | stackedBarIndex | expectedData
+  ${0}     | ${0}            | ${DATA[0]}
+  ${0}     | ${1}            | ${DATA[0]}
+  ${1}     | ${0}            | ${DATA[1]}
+  ${1}     | ${1}            | ${DATA[1]}
+`('fires mouse position with correct data on bar mouse enter, bar index $barIndex, stacked bar index $stackedBarIndex', ({ barIndex, stackedBarIndex, expectedData }) => {
+    const onBarMouseEnter = jest.fn()
+    render(<DashboardBarChart data={DATA} yNumTicks={3} onBarMouseEnter={onBarMouseEnter} />)
+    userEvent.hover(TestUtils.selectAll('DashboardBarChart__StackedBar-', TestUtils.selectAll('DashboardBarChart__Bar-')[barIndex])[stackedBarIndex])
+    expect(onBarMouseEnter).toHaveBeenCalledWith({ x: 48, y: 65 }, expectedData)
+  })
+})

+ 2 - 4
src/components/modules/DashboardModule/DashboardBarChart/DashboardBarChart.tsx

@@ -16,7 +16,7 @@ import * as React from 'react'
 import { observer } from 'mobx-react'
 import { observer } from 'mobx-react'
 import styled from 'styled-components'
 import styled from 'styled-components'
 
 
-import { ThemeProps } from '@src/components/Theme'
+import { ThemePalette, ThemeProps } from '@src/components/Theme'
 import BarChartNiceScale from './BarChartNiceScale'
 import BarChartNiceScale from './BarChartNiceScale'
 
 
 const Wrapper = styled.div<any>`
 const Wrapper = styled.div<any>`
@@ -94,9 +94,7 @@ type DataItem = {
 }
 }
 type Props = {
 type Props = {
   style?: any,
   style?: any,
-  // eslint-disable-next-line react/no-unused-prop-types
   data: DataItem[],
   data: DataItem[],
-  // eslint-disable-next-line react/no-unused-prop-types
   yNumTicks: number,
   yNumTicks: number,
   colors?: string[],
   colors?: string[],
   onBarMouseEnter?: (position: { x: number, y: number }, item: DataItem) => void,
   onBarMouseEnter?: (position: { x: number, y: number }, item: DataItem) => void,
@@ -192,7 +190,7 @@ class DashboardBarChart extends React.Component<Props> {
                   <StackedBar
                   <StackedBar
                     // eslint-disable-next-line react/no-array-index-key
                     // eslint-disable-next-line react/no-array-index-key
                     key={`${item.label}-${i}`}
                     key={`${item.label}-${i}`}
-                    background={this.props.colors ? this.props.colors[i % this.props.colors.length] : '#0044CA'}
+                    background={this.props.colors ? this.props.colors[i % this.props.colors.length] : ThemePalette.primary}
                     height={height}
                     height={height}
                     onMouseEnter={(evt: MouseEvent) => {
                     onMouseEnter={(evt: MouseEvent) => {
                       const onMouseEnter = this.props.onBarMouseEnter
                       const onMouseEnter = this.props.onBarMouseEnter

+ 1 - 1
src/components/modules/DashboardModule/DashboardExecutions/DashboardExecutions.tsx

@@ -149,7 +149,7 @@ type State = {
   tooltipPosition: { x: number, y: number },
   tooltipPosition: { x: number, y: number },
   tooltipData: TooltipData | null,
   tooltipData: TooltipData | null,
 }
 }
-const COLORS = ['#F91661', '#0044CB']
+const COLORS = [ThemePalette.alert, ThemePalette.primary]
 
 
 @observer
 @observer
 class DashboardExecutions extends React.Component<Props, State> {
 class DashboardExecutions extends React.Component<Props, State> {

+ 1 - 2
src/components/modules/TransferModule/TransferItemModal/TransferItemModal.tsx

@@ -34,7 +34,6 @@ import WizardStorage from '@src/components/modules/WizardModule/WizardStorage'
 import type {
 import type {
   UpdateData, TransferItemDetails, MigrationItemDetails,
   UpdateData, TransferItemDetails, MigrationItemDetails,
 } from '@src/@types/MainItem'
 } from '@src/@types/MainItem'
-import type { NavigationItem } from '@src/components/ui/Panel'
 import {
 import {
   Endpoint, EndpointUtils, StorageBackend, StorageMap,
   Endpoint, EndpointUtils, StorageBackend, StorageMap,
 } from '@src/@types/Endpoint'
 } from '@src/@types/Endpoint'
@@ -787,7 +786,7 @@ class TransferItemModal extends React.Component<Props, State> {
   }
   }
 
 
   render() {
   render() {
-    const navigationItems: NavigationItem[] = [
+    const navigationItems: Panel['props']['navigationItems'] = [
       {
       {
         value: 'source_options',
         value: 'source_options',
         label: 'Source Options',
         label: 'Source Options',

+ 2 - 2
src/components/ui/Lists/MainListItem/MainListItem.tsx → src/components/modules/TransferModule/TransferListItem/TransferListItem.tsx

@@ -117,7 +117,7 @@ type Props = {
   onSelectedChange: (value: boolean) => void,
   onSelectedChange: (value: boolean) => void,
 }
 }
 @observer
 @observer
-class MainListItem extends React.Component<Props> {
+class TransferListItem extends React.Component<Props> {
   getStatus() {
   getStatus() {
     return this.props.item.last_execution_status
     return this.props.item.last_execution_status
   }
   }
@@ -212,4 +212,4 @@ class MainListItem extends React.Component<Props> {
   }
   }
 }
 }
 
 
-export default MainListItem
+export default TransferListItem

+ 0 - 0
src/components/ui/Lists/MainListItem/images/arrow.svg → src/components/modules/TransferModule/TransferListItem/images/arrow.svg


+ 0 - 0
src/components/ui/Lists/MainListItem/images/schedule.svg → src/components/modules/TransferModule/TransferListItem/images/schedule.svg


+ 6 - 0
src/components/modules/TransferModule/TransferListItem/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "TransferListItem",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./TransferListItem.tsx"
+}

+ 3 - 3
src/components/ui/Lists/MainListItem/story.tsx → src/components/modules/TransferModule/TransferListItem/story.tsx

@@ -14,7 +14,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import { storiesOf } from '@storybook/react'
 import { storiesOf } from '@storybook/react'
-import MainListItem from '.'
+import TransferListItem from './TransferListItem'
 
 
 const item: any = {
 const item: any = {
   origin_endpoint_id: 'openstack',
   origin_endpoint_id: 'openstack',
@@ -32,7 +32,7 @@ const endpointType = (id: any) => id
 
 
 storiesOf('MainListItem', module)
 storiesOf('MainListItem', module)
   .add('completed', () => (
   .add('completed', () => (
-    <MainListItem
+    <TransferListItem
       item={item}
       item={item}
       endpointType={endpointType}
       endpointType={endpointType}
       selected={false}
       selected={false}
@@ -44,7 +44,7 @@ storiesOf('MainListItem', module)
     />
     />
   ))
   ))
   .add('running', () => (
   .add('running', () => (
-    <MainListItem
+    <TransferListItem
       item={item2}
       item={item2}
       endpointType={endpointType}
       endpointType={endpointType}
       selected={false}
       selected={false}

+ 0 - 0
src/components/ui/Lists/MainListItem/test.tsx → src/components/modules/TransferModule/TransferListItem/test.tsx


+ 1 - 1
src/components/smart/AssessmentsPage/AssessmentsPage.tsx

@@ -18,7 +18,7 @@ import { observer } from 'mobx-react'
 
 
 import FilterList from '@src/components/ui/Lists/FilterList'
 import FilterList from '@src/components/ui/Lists/FilterList'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
-import PageHeader from '@src/components/ui/PageHeader'
+import PageHeader from '@src/components/smart/PageHeader'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import DropdownFilterGroup from '@src/components/ui/Dropdowns/DropdownFilterGroup'
 import DropdownFilterGroup from '@src/components/ui/Dropdowns/DropdownFilterGroup'
 import AssessmentListItem from '@src/components/modules/AssessmentModule/AssessmentListItem'
 import AssessmentListItem from '@src/components/modules/AssessmentModule/AssessmentListItem'

+ 1 - 1
src/components/smart/DashboardPage/DashboardPage.tsx

@@ -26,7 +26,7 @@ import notificationStore from '@src/stores/NotificationStore'
 
 
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
-import PageHeader from '@src/components/ui/PageHeader'
+import PageHeader from '@src/components/smart/PageHeader'
 import DashboardContent from '@src/components/modules/DashboardModule/DashboardContent'
 import DashboardContent from '@src/components/modules/DashboardModule/DashboardContent'
 
 
 import Utils from '@src/utils/ObjectUtils'
 import Utils from '@src/utils/ObjectUtils'

+ 1 - 1
src/components/smart/EndpointsPage/EndpointsPage.tsx

@@ -19,7 +19,7 @@ import { observer } from 'mobx-react'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import FilterList from '@src/components/ui/Lists/FilterList'
 import FilterList from '@src/components/ui/Lists/FilterList'
-import PageHeader from '@src/components/ui/PageHeader'
+import PageHeader from '@src/components/smart/PageHeader'
 import EndpointListItem from '@src/components/modules/EndpointModule/EndpointListItem'
 import EndpointListItem from '@src/components/modules/EndpointModule/EndpointListItem'
 import AlertModal from '@src/components/ui/AlertModal'
 import AlertModal from '@src/components/ui/AlertModal'
 import Modal from '@src/components/ui/Modal'
 import Modal from '@src/components/ui/Modal'

+ 1 - 1
src/components/smart/LogsPage/LogsPage.tsx

@@ -18,7 +18,7 @@ import { observer } from 'mobx-react'
 
 
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
-import PageHeader from '@src/components/ui/PageHeader'
+import PageHeader from '@src/components/smart/PageHeader'
 import TabNavigation from '@src/components/ui/TabNavigation'
 import TabNavigation from '@src/components/ui/TabNavigation'
 
 
 import logStore from '@src/stores/LogStore'
 import logStore from '@src/stores/LogStore'

+ 3 - 3
src/components/smart/MigrationsPage/MigrationsPage.tsx

@@ -19,9 +19,8 @@ import { observer } from 'mobx-react'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import FilterList from '@src/components/ui/Lists/FilterList'
 import FilterList from '@src/components/ui/Lists/FilterList'
-import PageHeader from '@src/components/ui/PageHeader'
+import PageHeader from '@src/components/smart/PageHeader'
 import AlertModal from '@src/components/ui/AlertModal'
 import AlertModal from '@src/components/ui/AlertModal'
-import MainListItem from '@src/components/ui/Lists/MainListItem'
 
 
 import projectStore from '@src/stores/ProjectStore'
 import projectStore from '@src/stores/ProjectStore'
 import migrationStore from '@src/stores/MigrationStore'
 import migrationStore from '@src/stores/MigrationStore'
@@ -33,6 +32,7 @@ import { ThemePalette } from '@src/components/Theme'
 import replicaMigrationFields from '@src/components/modules/TransferModule/ReplicaMigrationOptions/replicaMigrationFields'
 import replicaMigrationFields from '@src/components/modules/TransferModule/ReplicaMigrationOptions/replicaMigrationFields'
 import { MigrationItem } from '@src/@types/MainItem'
 import { MigrationItem } from '@src/@types/MainItem'
 import userStore from '@src/stores/UserStore'
 import userStore from '@src/stores/UserStore'
+import TransferListItem from '@src/components/modules/TransferModule/TransferListItem'
 import migrationLargeImage from './images/migration-large.svg'
 import migrationLargeImage from './images/migration-large.svg'
 import migrationItemImage from './images/migration.svg'
 import migrationItemImage from './images/migration.svg'
 
 
@@ -254,7 +254,7 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
               }}
               }}
               dropdownActions={BulkActions}
               dropdownActions={BulkActions}
               renderItemComponent={options => (
               renderItemComponent={options => (
-                <MainListItem
+                <TransferListItem
                   {...options}
                   {...options}
                   image={migrationItemImage}
                   image={migrationItemImage}
                   endpointType={id => {
                   endpointType={id => {

+ 1 - 1
src/components/smart/MinionPoolsPage/MinionPoolsPage.tsx

@@ -22,7 +22,7 @@ import Modal from '@src/components/ui/Modal'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import FilterList from '@src/components/ui/Lists/FilterList'
 import FilterList from '@src/components/ui/Lists/FilterList'
-import PageHeader from '@src/components/ui/PageHeader'
+import PageHeader from '@src/components/smart/PageHeader'
 
 
 import type { Action as DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
 import type { Action as DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
 
 

+ 0 - 0
src/components/ui/PageHeader/PageHeader.tsx → src/components/smart/PageHeader/PageHeader.tsx


+ 0 - 0
src/components/ui/PageHeader/package.json → src/components/smart/PageHeader/package.json


+ 0 - 0
src/components/ui/PageHeader/story.tsx → src/components/smart/PageHeader/story.tsx


+ 1 - 1
src/components/smart/ProjectsPage/ProjectsPage.tsx

@@ -19,7 +19,7 @@ import { observer } from 'mobx-react'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import FilterList from '@src/components/ui/Lists/FilterList'
 import FilterList from '@src/components/ui/Lists/FilterList'
-import PageHeader from '@src/components/ui/PageHeader'
+import PageHeader from '@src/components/smart/PageHeader'
 import ProjectListItem from '@src/components/modules/ProjectModule/ProjectListItem'
 import ProjectListItem from '@src/components/modules/ProjectModule/ProjectListItem'
 
 
 import type { Project, RoleAssignment } from '@src/@types/Project'
 import type { Project, RoleAssignment } from '@src/@types/Project'

+ 3 - 3
src/components/smart/ReplicasPage/ReplicasPage.tsx

@@ -19,9 +19,8 @@ import { observer } from 'mobx-react'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import FilterList from '@src/components/ui/Lists/FilterList'
 import FilterList from '@src/components/ui/Lists/FilterList'
-import PageHeader from '@src/components/ui/PageHeader'
+import PageHeader from '@src/components/smart/PageHeader'
 import AlertModal from '@src/components/ui/AlertModal'
 import AlertModal from '@src/components/ui/AlertModal'
-import MainListItem from '@src/components/ui/Lists/MainListItem'
 import Modal from '@src/components/ui/Modal'
 import Modal from '@src/components/ui/Modal'
 import ReplicaExecutionOptions from '@src/components/modules/TransferModule/ReplicaExecutionOptions'
 import ReplicaExecutionOptions from '@src/components/modules/TransferModule/ReplicaExecutionOptions'
 import ReplicaMigrationOptions from '@src/components/modules/TransferModule/ReplicaMigrationOptions'
 import ReplicaMigrationOptions from '@src/components/modules/TransferModule/ReplicaMigrationOptions'
@@ -43,6 +42,7 @@ import { ThemePalette } from '@src/components/Theme'
 import configLoader from '@src/utils/Config'
 import configLoader from '@src/utils/Config'
 import { ReplicaItem } from '@src/@types/MainItem'
 import { ReplicaItem } from '@src/@types/MainItem'
 import userStore from '@src/stores/UserStore'
 import userStore from '@src/stores/UserStore'
+import TransferListItem from '@src/components/modules/TransferModule/TransferListItem'
 import replicaLargeImage from './images/replica-large.svg'
 import replicaLargeImage from './images/replica-large.svg'
 import replicaItemImage from './images/replica.svg'
 import replicaItemImage from './images/replica.svg'
 
 
@@ -352,7 +352,7 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
               onSelectedItemsChange={selectedReplicas => { this.setState({ selectedReplicas }) }}
               onSelectedItemsChange={selectedReplicas => { this.setState({ selectedReplicas }) }}
               onPaginatedItemsChange={paginatedReplicas => { this.handlePaginatedItemsChange(paginatedReplicas) }}
               onPaginatedItemsChange={paginatedReplicas => { this.handlePaginatedItemsChange(paginatedReplicas) }}
               renderItemComponent={options => (
               renderItemComponent={options => (
-                <MainListItem
+                <TransferListItem
                   {...options}
                   {...options}
                   image={replicaItemImage}
                   image={replicaItemImage}
                   showScheduleIcon={this.isReplicaScheduled(options.item.id)}
                   showScheduleIcon={this.isReplicaScheduled(options.item.id)}

+ 1 - 1
src/components/smart/UsersPage/UsersPage.tsx

@@ -19,7 +19,6 @@ import { observer } from 'mobx-react'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import FilterList from '@src/components/ui/Lists/FilterList'
 import FilterList from '@src/components/ui/Lists/FilterList'
-import PageHeader from '@src/components/ui/PageHeader'
 import UserListItem from '@src/components/modules/UserModule/UserListItem'
 import UserListItem from '@src/components/modules/UserModule/UserListItem'
 
 
 import type { User } from '@src/@types/User'
 import type { User } from '@src/@types/User'
@@ -27,6 +26,7 @@ import type { User } from '@src/@types/User'
 import projectStore from '@src/stores/ProjectStore'
 import projectStore from '@src/stores/ProjectStore'
 import userStore from '@src/stores/UserStore'
 import userStore from '@src/stores/UserStore'
 import configLoader from '@src/utils/Config'
 import configLoader from '@src/utils/Config'
+import PageHeader from '@src/components/smart/PageHeader'
 
 
 const Wrapper = styled.div<any>``
 const Wrapper = styled.div<any>``
 
 

+ 71 - 0
src/components/ui/AlertModal/AlertModal.spec.tsx

@@ -0,0 +1,71 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React from 'react'
+import { render } from '@testing-library/react'
+import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
+import TestUtils from '@tests/TestUtils'
+import AlertModal from './AlertModal'
+
+jest.mock('../StatusComponents/StatusImage/StatusImage', () => jest.fn(() => null))
+
+describe('AlertModal', () => {
+  it('renders confirmation as default with message and extra message', () => {
+    const message = 'message'
+    const extraMessage = 'extra message'
+    const { queryByText } = render((
+      <AlertModal
+        isOpen
+        message={message}
+        extraMessage={extraMessage}
+      />
+    ))
+    expect(TestUtils.select('AlertModal__Message')?.innerHTML).toBe(message)
+    expect(TestUtils.select('AlertModal__ExtraMessage')?.textContent).toBe(extraMessage)
+
+    expect(queryByText('No')).toBeTruthy()
+    expect(queryByText('Yes')).toBeTruthy()
+    expect(queryByText('Dismiss')).toBeNull()
+    expect(StatusImage).toHaveBeenCalledWith({ status: 'confirmation' }, {})
+  })
+
+  it('has correct buttons for errors', () => {
+    const { queryByText } = render((
+      <AlertModal
+        isOpen
+        message="message"
+        extraMessage="extra message"
+        type="error"
+      />
+    ))
+    expect(queryByText('Dismiss')).toBeTruthy()
+    expect(queryByText('No')).toBeNull()
+    expect(queryByText('Yes')).toBeNull()
+  })
+
+  it('renders loading', () => {
+    const { queryByText } = render((
+      <AlertModal
+        isOpen
+        message="message"
+        extraMessage="extra message"
+        type="loading"
+      />
+    ))
+    expect(queryByText('Dismiss')).toBeNull()
+    expect(queryByText('No')).toBeNull()
+    expect(queryByText('Yes')).toBeNull()
+    expect(StatusImage).toHaveBeenCalledWith({ status: 'RUNNING' }, {})
+  })
+})

+ 0 - 56
src/components/ui/AlertModal/test.tsx

@@ -1,56 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import TW from '../../../utils/TestWrapper'
-import AlertModal from '.'
-
-const wrap = props => new TW(shallow(<AlertModal {...props} />), 'aModal')
-
-describe('AlertModal Component', () => {
-  it('renders confirmation as default with message and extra message', () => {
-    let wrapper = wrap({ message: 'alert-message', extraMessage: 'alert-extra' })
-    expect(wrapper.findText('message')).toBe('alert-message')
-    expect(wrapper.findText('extraMessage')).toBe('alert-extra')
-    expect(wrapper.find('status').prop('status')).toBe('confirmation')
-    expect(wrapper.find('noButton').length).toBe(1)
-    expect(wrapper.find('yesButton').length).toBe(1)
-    expect(wrapper.find('dismissButton').length).toBe(0)
-  })
-
-  it('has correct buttons for confirmation', () => {
-    let wrapper = wrap({ message: 'alert-message', extraMessage: 'alert-extra' })
-    expect(wrapper.find('noButton').prop('secondary')).toBe(true)
-    expect(wrapper.find('yesButton').prop('secondary')).toBe(undefined)
-    expect(wrapper.find('noButton').shallow.dive().dive().text()).toBe('No')
-    expect(wrapper.find('yesButton').shallow.dive().dive().text()).toBe('Yes')
-  })
-
-  it('has correct button for error', () => {
-    let wrapper = wrap({ message: 'alert-message', extraMessage: 'alert-extra', type: 'error' })
-    expect(wrapper.find('dismissButton').length).toBe(1)
-  })
-
-  it('renders loading', () => {
-    let wrapper = wrap({ message: 'alert-message', extraMessage: 'alert-extra', type: 'loading' })
-    expect(wrapper.find('status').prop('status')).toBe('RUNNING')
-    expect(wrapper.find('noButton').length).toBe(0)
-    expect(wrapper.find('yesButton').length).toBe(0)
-    expect(wrapper.find('dismissButton').length).toBe(0)
-  })
-})
-
-
-

+ 46 - 0
src/components/ui/Arrow/Arrow.spec.tsx

@@ -0,0 +1,46 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import { ThemePalette } from '@src/components/Theme'
+import Arrow from './Arrow'
+
+describe('Arrow', () => {
+  it.each`
+    orientation
+    ${'up'}
+    ${'down'}
+    ${'left'}
+    ${'right'}
+  `('renders the $orientation orientation', ({ orientation }) => {
+    render(<Arrow orientation={orientation} />)
+    expect(TestUtils.select('Arrow__Wrapper')?.getAttribute('orientation')).toBe(orientation)
+  })
+
+  it('renderes with primary colors', () => {
+    const { rerender } = render(<Arrow primary />)
+    expect(document.querySelector(`g[stroke="${ThemePalette.primary}"]`)).toBeTruthy()
+    rerender(<Arrow />)
+    expect(document.querySelector(`g[stroke="${ThemePalette.grayscale[4]}"]`)).toBeTruthy()
+  })
+
+  it('renderes with primary colors', () => {
+    const { rerender } = render(<Arrow primary />)
+    expect(document.querySelector(`g[stroke="${ThemePalette.primary}"]`)).toBeTruthy()
+    rerender(<Arrow />)
+    expect(document.querySelector(`g[stroke="${ThemePalette.grayscale[4]}"]`)).toBeTruthy()
+  })
+})

+ 36 - 0
src/components/ui/AutocompleteInput/AutocompleteInput.spec.tsx

@@ -0,0 +1,36 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import AutocompleteInput from './AutocompleteInput'
+
+describe('AutocompleteInput', () => {
+  it('renders correct data', () => {
+    render(<AutocompleteInput value="searching" onChange={() => { }} />)
+    expect(TestUtils.selectInput('TextInput__Input')!.value).toBe('searching')
+  })
+
+  it('calls focus and blur', () => {
+    const onFocus = jest.fn()
+    const onBlur = jest.fn()
+    render(<AutocompleteInput value="" onChange={() => { }} onFocus={onFocus} onBlur={onBlur} />)
+    const inputElement = TestUtils.select('TextInput__Input')
+    inputElement?.focus()
+    expect(onFocus).toHaveBeenCalled()
+    inputElement?.blur()
+    expect(onBlur).toHaveBeenCalled()
+  })
+})

+ 37 - 0
src/components/ui/Button/Button.spec.tsx

@@ -0,0 +1,37 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import { ThemePalette } from '@src/components/Theme'
+import Button from './Button'
+
+describe('Button', () => {
+  it('should render with different style props', () => {
+    const { rerender } = render(<Button disabled />)
+    expect(document.querySelector('button')?.hasAttribute('disabled')).toBeTruthy()
+    expect(TestUtils.rgbToHex(window.getComputedStyle(document.querySelector('button')!).backgroundColor)).toBe(ThemePalette.primary)
+
+    rerender(<Button secondary />)
+    expect(TestUtils.rgbToHex(window.getComputedStyle(document.querySelector('button')!).backgroundColor)).toBe(ThemePalette.secondaryLight)
+  })
+
+  it('fires click', () => {
+    const onClick = jest.fn()
+    render(<Button onClick={onClick} />)
+    document.querySelector('button')?.click()
+    expect(onClick).toHaveBeenCalled()
+  })
+})

+ 0 - 43
src/components/ui/Button/test.tsx

@@ -1,43 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import sinon from 'sinon'
-import Button from '.'
-
-const wrap = props => shallow(<Button {...props} />)
-
-describe('Button Component', () => {
-  it('renders with different combination of props', () => {
-    let wrapper = wrap({ disabled: true })
-    expect(wrapper.prop('disabled')).toBe(true)
-    wrapper = wrap({ primary: true })
-    expect(wrapper.prop('disabled')).toBe(undefined)
-    expect(wrapper.prop('primary')).toBe(true)
-    wrapper = wrap({ disabled: true, primary: true })
-    expect(wrapper.prop('disabled')).toBe(true)
-    expect(wrapper.prop('primary')).toBe(true)
-  })
-
-  it('dispatches click event', () => {
-    const onButtonClick = sinon.spy()
-    const wrapper = wrap({ onClick: onButtonClick })
-    wrapper.simulate('click')
-    expect(onButtonClick.calledOnce).toBe(true)
-  })
-})
-
-
-

+ 39 - 0
src/components/ui/Checkbox/Checkbox.spec.tsx

@@ -0,0 +1,39 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+
+import TestUtils from '@tests/TestUtils'
+import Checkbox from './Checkbox'
+
+describe('Checkbox', () => {
+  it('dispatches change on space key', () => {
+    const onChange = jest.fn()
+    const { rerender } = render(<Checkbox onChange={onChange} />)
+    userEvent.type(TestUtils.select('Checkbox__Wrapper')!, ' ')
+    expect(onChange).toHaveBeenCalledWith(true)
+    rerender(<Checkbox onChange={onChange} checked />)
+    userEvent.type(TestUtils.select('Checkbox__Wrapper')!, ' ')
+    expect(onChange).toHaveBeenCalledWith(false)
+  })
+
+  it('doesn\'t dispatch change if disabled', () => {
+    const onChange = jest.fn()
+    render(<Checkbox onChange={onChange} disabled />)
+    userEvent.type(TestUtils.select('Checkbox__Wrapper')!, ' ')
+    expect(onChange).not.toHaveBeenCalled()
+  })
+})

+ 0 - 44
src/components/ui/Checkbox/test.tsx

@@ -1,44 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import sinon from 'sinon'
-import Checkbox from '../Checkbox'
-
-const wrap = props => shallow(<Checkbox {...props} />)
-
-describe('Checkbox Component', () => {
-  it('passes `checked` to the component', () => {
-    let wrapper = wrap({ checked: true, onChange: () => {} })
-    expect(wrapper.prop('checked')).toBe(true)
-  })
-
-  it('calls `onChange` with correct value, on click', () => {
-    let onChange = sinon.spy()
-    let wrapper = wrap({ checked: false, onChange })
-    wrapper.simulate('click')
-    expect(onChange.args[0][0]).toBe(true)
-  })
-
-  it('doesn\'t call `onChange` if disabled', () => {
-    let onChange = sinon.spy()
-    let wrapper = wrap({ checked: false, onChange, disabled: true })
-    wrapper.simulate('click')
-    expect(onChange.notCalled).toBe(true)
-  })
-})
-
-
-

+ 14 - 15
src/components/ui/StatusComponents/StatusPill/test.tsx → src/components/ui/CopyButton/CopyButton.spec.tsx

@@ -1,5 +1,5 @@
 /*
 /*
-Copyright (C) 2017  Cloudbase Solutions SRL
+Copyright (C) 2021  Cloudbase Solutions SRL
 This program is free software: you can redistribute it and/or modify
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
 it under the terms of the GNU Affero General Public License as
 published by the Free Software Foundation, either version 3 of the
 published by the Free Software Foundation, either version 3 of the
@@ -13,22 +13,21 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
 import React from 'react'
 import React from 'react'
-import { shallow } from 'enzyme'
-import StatusPill from '.'
+import { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import CopyButton from './CopyButton'
 
 
-const wrap = props => shallow(<StatusPill {...props} />)
-
-describe('StatusPill Component', () => {
-  it('renders label if given', () => {
-    let wrapper = wrap({ label: 'the_value', status: 'COMPLETED' })
-    expect(wrapper.dive().text()).toBe('the_value')
+describe('CopyButton', () => {
+  it('renders with no opacity', () => {
+    render(<CopyButton />)
+    expect(window.getComputedStyle(TestUtils.select('CopyButton__Wrapper')!).opacity).toBe('0')
   })
   })
 
 
-  it('renders status as label if no label is given', () => {
-    let wrapper = wrap({ status: 'COMPLETED' })
-    expect(wrapper.dive().text()).toBe('COMPLETED')
+  it('dispatches click', () => {
+    const onClick = jest.fn()
+    render(<CopyButton onClick={onClick} />)
+    const button = TestUtils.select('CopyButton__Wrapper') as HTMLElement
+    button.click()
+    expect(onClick).toHaveBeenCalled()
   })
   })
 })
 })
-
-
-

+ 38 - 0
src/components/ui/CopyMultilineValue/CopyMultilineValue.spec.tsx

@@ -0,0 +1,38 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import DomUtils from '@src/utils/DomUtils'
+import CopyMultineValue from './CopyMultilineValue'
+
+jest.mock('../../../utils/DomUtils')
+
+describe('CopyMultilineValue', () => {
+  it('copies value to clipboard', () => {
+    const onCopy = jest.fn()
+    render(<CopyMultineValue value="test value" onCopy={onCopy} />)
+    TestUtils.select('CopyMultilineValue__Wrapper')!.click()
+    expect(DomUtils.copyTextToClipboard).toHaveBeenCalledWith('test value')
+    expect(onCopy).toHaveBeenCalledWith('test value')
+  })
+
+  it('transforms dangerous HTML', () => {
+    const onCopy = jest.fn()
+    render(<CopyMultineValue useDangerousHtml onCopy={onCopy} value="this<br />is <b>OK</b>" />)
+    TestUtils.select('CopyMultilineValue__Wrapper')!.click()
+    expect(onCopy).toHaveBeenCalledWith('this\nis OK')
+  })
+})

+ 0 - 38
src/components/ui/CopyMultilineValue/test.tsx

@@ -1,38 +0,0 @@
-/*
-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/>.
-*/
-
-import React from 'react'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import CopyMultilineValue from '../CopyMultilineValue'
-
-const wrap = props => shallow(<CopyMultilineValue value="" {...props} />)
-
-describe('CopyMultilineValue Component', () => {
-  it('renders `value`', () => {
-    const wrapper = wrap({ value: 'the_value' })
-    expect(wrapper.dive().text()).toBe('the_value<Styled(CopyButton) />')
-  })
-
-  it('copies `value` to clipboard', () => {
-    const onCopy = sinon.spy()
-    const wrapper = wrap({ value: 'the_value', onCopy })
-    wrapper.simulate('click')
-    expect(onCopy.calledOnce).toBe(true)
-    expect(onCopy.args[0][0]).toBe('the_value')
-  })
-})
-
-
-

+ 34 - 0
src/components/ui/CopyValue/CopyValue.spec.tsx

@@ -0,0 +1,34 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import DomUtils from '@src/utils/DomUtils'
+import CopyValue from './CopyValue'
+
+jest.mock('../../../utils/DomUtils')
+
+describe('CopyValue', () => {
+  it('copies value to clipboard', () => {
+    render(<CopyValue value="value" />)
+    TestUtils.select('CopyValue__Wrapper')!.click()
+    expect(DomUtils.copyTextToClipboard).toHaveBeenCalledWith('value')
+  })
+
+  it('capitalizes the value', () => {
+    render(<CopyValue capitalize value="value" />)
+    expect(window.getComputedStyle(TestUtils.select('CopyValue__Wrapper')!).textTransform).toBe('capitalize')
+  })
+})

+ 0 - 39
src/components/ui/CopyValue/test.tsx

@@ -1,39 +0,0 @@
-/*
-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/>.
-*/
-
-import React from 'react'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TestWrapper from '../../../utils/TestWrapper'
-import CopyValue from '../CopyValue'
-
-const wrap = props => new TestWrapper(shallow(<CopyValue value="the_value" {...props} />), 'copyValue')
-
-describe('CopyValue Component', () => {
-  it('renders `value`', () => {
-    const wrapper = wrap()
-    expect(wrapper.findText('value')).toBe('the_value')
-  })
-
-  it('copies `value` to clipboard', () => {
-    const onCopy = sinon.spy()
-    const wrapper = wrap({ onCopy })
-    wrapper.simulate('click')
-    expect(onCopy.calledOnce).toBe(true)
-    expect(onCopy.args[0][0]).toBe('the_value')
-  })
-})
-
-
-

+ 61 - 0
src/components/ui/DatetimePicker/DatetimePicker.spec.tsx

@@ -0,0 +1,61 @@
+/*
+Copyright (C) 2021  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 moment from 'moment'
+import { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import DatetimePicker from './DatetimePicker'
+
+const DATE = new Date('2021-11-12T12:32:44.426Z')
+
+describe('DatetimePicker', () => {
+  it('renders date value in UTC timezone in dropdown label', () => {
+    render(
+      <DatetimePicker
+        onChange={() => { }}
+        timezone="utc"
+        value={DATE}
+      />,
+    )
+
+    const expected = moment(DATE)
+      .add(new Date().getTimezoneOffset(), 'minutes')
+      .format('DD/MM/YYYY hh:mm A')
+
+    expect(TestUtils.select('DropdownButton__Label')?.innerHTML).toEqual(expected)
+  })
+
+  it('changes the date', () => {
+    render(
+      <DatetimePicker
+        onChange={() => { }}
+        timezone="utc"
+        value={DATE}
+      />,
+    )
+    expect(TestUtils.select('DatetimePicker__Portal')).toBeNull()
+    TestUtils.select('DropdownButton__Wrapper')?.click()
+    expect(TestUtils.select('DatetimePicker__Portal')).not.toBeNull()
+    const firstDay = document.querySelector<HTMLElement>('td.rdtDay[data-value="1"]')
+    firstDay?.click()
+
+    const expected = moment(DATE)
+      .set('date', 1)
+      .add(new Date().getTimezoneOffset(), 'minutes')
+      .format('DD/MM/YYYY hh:mm A')
+
+    expect(TestUtils.select('DropdownButton__Label')?.innerHTML).toEqual(expected)
+  })
+})

+ 0 - 42
src/components/ui/DatetimePicker/test.tsx

@@ -1,42 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import moment from 'moment'
-import sinon from 'sinon'
-import TestWrapper from '../../../utils/TestWrapper'
-import DatetimePicker from '.'
-
-const wrap = props => new TestWrapper(shallow(<DatetimePicker timezone="local" {...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.find('dropdownButton').prop('value')).toBe(label)
-  })
-
-  it('renders date value in UTC timezone in dropdown label', () => {
-    let onChange = sinon.spy()
-    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.find('dropdownButton').prop('value')).toBe(label)
-  })
-})
-
-
-

+ 94 - 0
src/components/ui/Dropdowns/ActionDropdown/ActionDropdown.spec.tsx

@@ -0,0 +1,94 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import ActionDropdown, { Action } from './ActionDropdown'
+
+const ACTIONS: Action[] = [
+  {
+    label: 'Action 1',
+    title: 'Action 1 Description',
+    action: jest.fn(),
+  },
+  {
+    label: 'Action 2',
+    disabled: true,
+    action: jest.fn(),
+
+  },
+  {
+    label: 'Action 3',
+    loading: true,
+    action: jest.fn(),
+  },
+  {
+    label: 'Action 4',
+    hidden: true,
+    action: jest.fn(),
+  },
+]
+
+describe('ActionDropdown', () => {
+  it('renders button label', () => {
+    const { rerender } = render(<ActionDropdown actions={ACTIONS} />)
+    expect(TestUtils.select('DropdownButton__Label')?.textContent).toBe('Actions')
+    rerender(<ActionDropdown actions={ACTIONS} label="Actions Label" />)
+    expect(TestUtils.select('DropdownButton__Label')?.textContent).toBe('Actions Label')
+  })
+
+  it('renders only visible actions', () => {
+    render(<ActionDropdown actions={ACTIONS} />)
+    TestUtils.select('DropdownButton__Wrapper')!.click()
+    expect(TestUtils.selectAll('ActionDropdown__ListItem').length).toBe(3)
+    TestUtils.selectAll('ActionDropdown__ListItem').forEach((item, index) => {
+      expect(item.textContent).toBe(ACTIONS[index].label)
+    })
+  })
+
+  it('renders actions with props', () => {
+    render(<ActionDropdown actions={ACTIONS} />)
+    TestUtils.select('DropdownButton__Wrapper')!.click()
+    TestUtils.selectAll('ActionDropdown__ListItem').forEach((item, index) => {
+      if (ACTIONS[index].disabled) {
+        expect(item.hasAttribute('disabled')).toBe(true)
+      } else {
+        expect(item.hasAttribute('disabled')).toBe(false)
+      }
+      if (ACTIONS[index].title) {
+        expect(item.getAttribute('title')).toBe(ACTIONS[index].title)
+      }
+      if (ACTIONS[index].loading) {
+        expect(TestUtils.select('StatusIcon__Wrapper', item)).toBeTruthy()
+      } else {
+        expect(TestUtils.select('StatusIcon__Wrapper', item)).toBeFalsy()
+      }
+    })
+  })
+
+  it('fires click events correctly', () => {
+    render(<ActionDropdown actions={ACTIONS} />)
+    TestUtils.select('DropdownButton__Wrapper')!.click()
+    TestUtils.selectAll('ActionDropdown__ListItem').forEach((item, index) => {
+      item.click()
+      if (ACTIONS[index].disabled || ACTIONS[index].loading) {
+        expect(ACTIONS[index].action).not.toHaveBeenCalled()
+      } else {
+        TestUtils.select('DropdownButton__Wrapper')!.click()
+        expect(ACTIONS[index].action).toHaveBeenCalled()
+      }
+    })
+  })
+})

+ 0 - 93
src/components/ui/Dropdowns/ActionDropdown/test.tsx

@@ -1,93 +0,0 @@
-/*
-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 sinon from 'sinon'
-import { shallow, mount } from 'enzyme'
-
-import TW from '../../../utils/TestWrapper'
-import ActionDropdown, { TEST_ID } from '.'
-import type { Props, Action } from '.'
-
-const defaultActions: Action[] = [
-  { label: 'Action1', action: () => { } },
-  { label: 'Action2', action: () => { }, color: 'red' },
-  { label: 'Action3', action: () => { }, disabled: true },
-  { label: 'Action4', action: () => { }, hidden: true },
-  { label: 'Action5', action: () => { } },
-]
-const defaultProps: Props = {
-  label: 'Actions',
-  actions: defaultActions,
-}
-
-const wrap = (props: Props) => new TW(shallow(<ActionDropdown {...props} />), TEST_ID)
-const domWrap = (props: Props) => new TW(mount(<ActionDropdown {...props} />), TEST_ID)
-
-describe('ActionDropdown Component', () => {
-  it('renders the dropdown button with the correct label', () => {
-    let wrapper = wrap(defaultProps)
-    expect(wrapper.find('dropdownButton').prop('value')).toBe(defaultProps.label)
-  })
-
-  it('opens list on click', () => {
-    let wrapper = domWrap(defaultProps)
-    expect(wrapper.findDiv('list').length).toBe(0)
-    wrapper.findDiv('dropdownButton').simulate('click')
-    expect(wrapper.findDiv('list').length).toBe(1)
-  })
-
-  it('renders only visible actions labels', () => {
-    let wrapper = domWrap(defaultProps)
-    wrapper.findDiv('dropdownButton').simulate('click')
-    defaultActions.forEach(a => {
-      if (a.hidden) {
-        expect(wrapper.findDiv(`listItem-${a.label}`).length).toBe(0)
-      } else {
-        expect(wrapper.findDiv(`listItem-${a.label}`).length).toBe(1)
-        expect(wrapper.findDiv(`listItem-${a.label}`).text()).toBe(a.label)
-      }
-    })
-  })
-
-  it('renders correct props for all actions', () => {
-    let wrapper = domWrap(defaultProps)
-    wrapper.findDiv('dropdownButton').simulate('click')
-    defaultActions.filter(a => !a.hidden).forEach(a => {
-      expect(wrapper.findDiv(`listItem-${a.label}`).prop('color')).toBe(a.color)
-      expect(wrapper.findDiv(`listItem-${a.label}`).prop('disabled')).toBe(a.disabled)
-    })
-  })
-
-  it('dispaches correct actions on action click', () => {
-    let props: Props = { ...defaultProps }
-    let enabledAction = props.actions[1]
-    let disabledAction = props.actions[2]
-    enabledAction.action = sinon.spy()
-    disabledAction.action = sinon.spy()
-
-    let wrapper = domWrap(props)
-    wrapper.findDiv('dropdownButton').simulate('click')
-
-    let enabledActionWrapper = wrapper.findDiv(`listItem-${enabledAction.label}`)
-    let disabledActionWrapper = wrapper.findDiv(`listItem-${disabledAction.label}`)
-    enabledActionWrapper.simulate('click')
-    disabledActionWrapper.simulate('click')
-    expect(enabledAction.action.called).toBe(true)
-    expect(disabledAction.action.called).toBe(false)
-  })
-})
-
-
-

+ 65 - 0
src/components/ui/Dropdowns/AutocompleteDropdown/AutocompleteDropdown.spec.tsx

@@ -0,0 +1,65 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import TestUtils from '@tests/TestUtils'
+import AutocompleteDropdown from './AutocompleteDropdown'
+
+const ITEMS = [
+  { label: 'Label A 1', value: 'item 1' },
+  { label: 'Label A 2', value: 'item 2' },
+  { label: 'Label B 3', value: 'item 3' },
+  { label: 'Label B 4', value: 'item 4' },
+  { label: 'Label B 5', value: 'item 5' },
+]
+
+describe('AutocompleteDropdown', () => {
+  it('renders', () => {
+    render(<AutocompleteDropdown />)
+    expect(TestUtils.select('AutocompleteDropdown__Wrapper')).toBeTruthy()
+  })
+
+  it('autocompletes a search value', () => {
+    render(<AutocompleteDropdown items={ITEMS} />)
+    userEvent.type(document.querySelector('input')!, 'Label B')
+
+    expect(TestUtils.selectAll('AutocompleteDropdown__ListItem-').length).toBe(3)
+    TestUtils.selectAll('AutocompleteDropdown__ListItem-').forEach(item => {
+      expect(item.textContent).toContain('Label B')
+    })
+  })
+
+  it('fires change on autocomplete item click', () => {
+    const onChange = jest.fn()
+    render(<AutocompleteDropdown items={ITEMS} onChange={onChange} />)
+    userEvent.type(document.querySelector('input')!, 'Label B')
+
+    TestUtils.selectAll('AutocompleteDropdown__ListItem-')[1].click()
+
+    expect(onChange).toHaveBeenCalledWith(ITEMS[3])
+  })
+
+  it('display message if no items were found', () => {
+    render(<AutocompleteDropdown items={ITEMS} noItemsMessage="No results found!" />)
+    userEvent.type(document.querySelector('input')!, 'Label Z')
+    expect(TestUtils.select('AutocompleteDropdown__SearchNotFound')?.textContent).toBe('No results found!')
+  })
+
+  it('shows selected item', () => {
+    render(<AutocompleteDropdown items={ITEMS} selectedItem={ITEMS[1]} />)
+    expect(document.querySelector('input')?.value).toBe(ITEMS[1].label)
+  })
+})

+ 65 - 0
src/components/ui/Dropdowns/Dropdown/Dropdown.spec.tsx

@@ -0,0 +1,65 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import Dropdown from '@src/components/ui/Dropdowns/Dropdown'
+import TestUtils from '@tests/TestUtils'
+
+const ITEMS = [
+  { label: 'Label A 1', value: 'item 1' },
+  { label: 'Label A 2', value: 'item 2' },
+  { label: 'Label A 2', value: 'item 3' },
+]
+
+describe('Dropdown', () => {
+  it('renders', () => {
+    render(<Dropdown items={ITEMS} />)
+    expect(TestUtils.select('Dropdown__Wrapper')).toBeTruthy()
+  })
+
+  it('opens the dropdown list with the correct number of items', () => {
+    render(<Dropdown items={ITEMS} />)
+    expect(TestUtils.select('DropdownButton__Label')?.textContent).toBe('Select an item')
+    const button = TestUtils.select('DropdownButton__Wrapper')
+    expect(button).toBeTruthy()
+    button?.click()
+    expect(TestUtils.selectAll('Dropdown__ListItem-').length).toBe(3)
+  })
+
+  it('displays duplicated label', () => {
+    render(<Dropdown items={ITEMS} />)
+    TestUtils.select('DropdownButton__Wrapper')?.click()
+    expect(TestUtils.selectAll('Dropdown__DuplicatedLabel').length).toBe(2)
+    const duplicatedItems = [ITEMS[1], ITEMS[2]]
+    TestUtils.selectAll('Dropdown__DuplicatedLabel').forEach((item, index) => {
+      expect(item.textContent).toBe(`(${duplicatedItems[index].value})`)
+    })
+  })
+
+  it('renders selected item', () => {
+    render(<Dropdown items={ITEMS} selectedItem={ITEMS[0]} />)
+    expect(TestUtils.select('DropdownButton__Label')?.textContent).toBe('Label A 1')
+  })
+
+  it('fires change on item click', () => {
+    const onChange = jest.fn()
+    render(<Dropdown items={ITEMS} onChange={onChange} />)
+    const button = TestUtils.select('DropdownButton__Wrapper')
+    button!.click()
+    const items = TestUtils.selectAll('Dropdown__ListItem-')
+    items[1]!.click()
+    expect(onChange).toHaveBeenCalledWith(ITEMS[1])
+  })
+})

+ 0 - 53
src/components/ui/Dropdowns/Dropdown/test.tsx

@@ -1,53 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import TestWrapper from '@src/utils/TestWrapper'
-import Dropdown from '.'
-
-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' },
-]
-
-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('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.find('dropdownButton').prop('value')).toBe('no selection')
-  })
-})
-
-
-

+ 42 - 0
src/components/ui/Dropdowns/DropdownButton/DropdownButton.spec.tsx

@@ -0,0 +1,42 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import DropdownButton from '@src/components/ui/Dropdowns/DropdownButton'
+import TestUtils from '@tests/TestUtils'
+
+describe('DropdownButton', () => {
+  it('renders the value', () => {
+    render(<DropdownButton value="The Value" />)
+    expect(TestUtils.select('DropdownButton__Label')?.textContent).toBe('The Value')
+  })
+
+  it('fires click on click', () => {
+    const onClick = jest.fn()
+    const { getByText } = render(<DropdownButton value="The Value" onClick={onClick} />)
+    getByText('The Value').click()
+    expect(onClick).toHaveBeenCalled()
+  })
+
+  it('doesn\'t fire click if disabled or disabledLoading', () => {
+    const onClick = jest.fn()
+    const { getByText, rerender } = render(<DropdownButton value="The Value" onClick={onClick} disabled />)
+    getByText('The Value').click()
+    expect(onClick).not.toHaveBeenCalled()
+    rerender(<DropdownButton value="The Value" onClick={onClick} disabledLoading />)
+    getByText('The Value').click()
+    expect(onClick).not.toHaveBeenCalled()
+  })
+})

+ 0 - 45
src/components/ui/Dropdowns/DropdownButton/test.tsx

@@ -1,45 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TestWrapper from '@src/utils/TestWrapper'
-import DropdownButton from '.'
-
-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.findText('value')).toBe('the_value')
-  })
-
-  it('calls click handler', () => {
-    let onClick = sinon.spy()
-    let wrapper = wrap({ onClick })
-    wrapper.simulate('click')
-    expect(onClick.calledOnce).toBe(true)
-  })
-
-  it('doesn\'t call click handler if disabled', () => {
-    let onClick = sinon.spy()
-    let wrapper = wrap({ onClick, disabled: true })
-    wrapper.simulate('click')
-    expect(onClick.notCalled).toBe(true)
-  })
-})
-
-
-

+ 49 - 0
src/components/ui/Dropdowns/DropdownFilter/DropdownFilter.spec.tsx

@@ -0,0 +1,49 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import DropdownFilter from '@src/components/ui/Dropdowns/DropdownFilter'
+import TestUtils from '@tests/TestUtils'
+import userEvent from '@testing-library/user-event'
+
+describe('DropdownFilter', () => {
+  it('renders ', () => {
+    render(<DropdownFilter />)
+    expect(TestUtils.select('DropdownFilter__Wrapper')).toBeTruthy()
+  })
+
+  it('renders the dropdown', () => {
+    render(<DropdownFilter />)
+    expect(TestUtils.select('SearchInput__Wrapper')).toBeFalsy()
+    TestUtils.select('DropdownFilter__Button')!.click()
+    expect(TestUtils.select('SearchInput__Wrapper')).toBeTruthy()
+  })
+
+  it('displays the search input value', () => {
+    render(<DropdownFilter searchValue="Search Value" />)
+    TestUtils.select('DropdownFilter__Button')!.click()
+    expect(TestUtils.selectInput('TextInput__Input')!.value).toBe('Search Value')
+  })
+
+  it('fires change on search input value change', () => {
+    const onChange = jest.fn()
+    render(<DropdownFilter onSearchChange={onChange} />)
+    TestUtils.select('DropdownFilter__Button')!.click()
+    userEvent.type(TestUtils.select('TextInput__Input')!, 'Search')
+    expect(onChange).toBeCalledTimes(6)
+    expect(onChange.mock.calls[0][0]).toBe('S')
+    expect(onChange.mock.calls[5][0]).toBe('h')
+  })
+})

+ 0 - 37
src/components/ui/Dropdowns/DropdownFilter/test.tsx

@@ -1,37 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import TestWrapper from '@src/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)
-  })
-})
-
-
-

+ 49 - 0
src/components/ui/Dropdowns/DropdownFilterGroup/DropdownFilterGroup.spec.tsx

@@ -0,0 +1,49 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import DropdownFilterGroup from '@src/components/ui/Dropdowns/DropdownFilterGroup'
+import TestUtils from '@tests/TestUtils'
+
+const ITEMS = [
+  {
+    items: [
+      { label: 'Item B 2', value: 'itemb2' },
+    ],
+    key: 'group1',
+  },
+  {
+    items: [
+      { label: 'Item 1', value: 'item1' },
+      { label: 'Item 2', value: 'item2' },
+    ],
+    key: 'group2',
+  },
+]
+
+describe('DropdownFilterGroup', () => {
+  it('renders the correct number of DropdownLink components', () => {
+    render(<DropdownFilterGroup items={ITEMS} />)
+    expect(TestUtils.selectAll('DropdownLink__Wrapper')).toHaveLength(ITEMS.length)
+  })
+
+  it('opens the DropdownLink component with the correct items', () => {
+    render(<DropdownFilterGroup items={ITEMS} />)
+    const dropdownLinks = TestUtils.selectAll('DropdownLink__LinkButton')
+    dropdownLinks[1].click()
+    expect(TestUtils.selectAll('DropdownLink__ListItem-')).toHaveLength(ITEMS[1].items.length)
+    expect(TestUtils.selectAll('DropdownLink__ListItemLabel')[1].textContent).toBe(ITEMS[1].items[1].label)
+  })
+})

+ 0 - 48
src/components/ui/Dropdowns/DropdownFilterGroup/test.tsx

@@ -1,48 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import TW from '@src/utils/TestWrapper'
-import DropdownFilterGroup from '.'
-
-const dropdowns = [
-  {
-    key: 'dropdown1',
-    items: [{ label: 'Dropdown 1 Item 1', value: 'dropdown1_item1_value' }],
-  },
-  {
-    key: 'dropdown2',
-    items: [{ label: 'Dropdown 2 Item 1', value: 'dropdown2_item1_value' }],
-    selectedItem: 'dropdown2_item1_value',
-  },
-]
-
-const wrap = props => new TW(shallow(
-  <DropdownFilterGroup items={dropdowns} {...props} />
-), 'dfGroup')
-
-describe('DropdownFilterGroup Component', () => {
-  it('renders correct dropdowns', () => {
-    let wrapper = wrap()
-    expect(wrapper.findPartialId('dropdown-').length).toBe(dropdowns.length)
-    dropdowns.forEach(dropdown => {
-      expect(wrapper.find(`dropdown-${dropdown.key}`).prop('items')[0].value).toBe(dropdown.items[0].value)
-      expect(wrapper.find(`dropdown-${dropdown.key}`).prop('selectedItem')).toBe(dropdown.selectedItem || undefined)
-    })
-  })
-})
-
-
-

+ 74 - 0
src/components/ui/Dropdowns/DropdownInput/DropdownInput.spec.tsx

@@ -0,0 +1,74 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+
+import TestUtils from '@tests/TestUtils'
+
+import DropdownInput from '.'
+
+const ITEMS = [
+  { label: 'Item 1', value: 'item-1' },
+  { label: 'Item 2', value: 'item-2' },
+  { label: 'Item 3', value: 'item-3' },
+]
+
+describe('DropdownInput', () => {
+  it('renders with the correct item label', () => {
+    render(
+      <DropdownInput
+        items={ITEMS}
+        selectedItem={ITEMS[1].value}
+        onInputChange={() => { }}
+        onItemChange={() => { }}
+        inputValue="test"
+      />,
+    )
+    expect(TestUtils.select('DropdownLink__Label')?.textContent).toBe('Item 2')
+    expect(TestUtils.selectInput('TextInput__Input')!.value).toBe('test')
+  })
+
+  it('fires input change', () => {
+    const onInputChange = jest.fn()
+    render(
+      <DropdownInput
+        items={ITEMS}
+        selectedItem={ITEMS[1].value}
+        onInputChange={onInputChange}
+        onItemChange={() => { }}
+        inputValue="test"
+      />,
+    )
+    userEvent.type(TestUtils.select('TextInput__Input')!, 'test2')
+    expect(onInputChange).toHaveBeenCalledWith('test2')
+  })
+
+  it('fires item change', () => {
+    const onItemChange = jest.fn()
+    render(
+      <DropdownInput
+        items={ITEMS}
+        selectedItem={ITEMS[1].value}
+        onInputChange={() => { }}
+        onItemChange={onItemChange}
+        inputValue="test"
+      />,
+    )
+    userEvent.click(TestUtils.select('DropdownLink__Label')!)
+    userEvent.click(TestUtils.selectAll('DropdownLink__ListItem-')[1])
+    expect(onItemChange).toHaveBeenCalledWith(ITEMS[1])
+  })
+})

+ 0 - 72
src/components/ui/Dropdowns/DropdownInput/test.tsx

@@ -1,72 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import TW from '@src/utils/TestWrapper'
-import DropdownInput from '.'
-
-type ItemType = {
-  label: string,
-  value: string,
-  [prop: string]: any,
-}
-type Props = {
-  items: ItemType[],
-  selectedItem: string,
-  onItemChange: (item: ItemType) => void,
-  inputValue: string,
-  onInputChange: (value: string) => void,
-  placeholder?: string,
-  highlight?: boolean,
-  disabled?: boolean,
-}
-
-const wrap = (props: Props) => new TW(shallow(
-  <DropdownInput {...props} />
-), 'ddInput')
-
-const items = [
-  { label: 'Item 1', value: 'item-1' },
-  { label: 'Item 2', value: 'item-2' },
-]
-
-describe('DropdownInput Component', () => {
-  it('renders link with correct data', () => {
-    let wrapper = wrap({
-      items,
-      selectedItem: 'item-2',
-      onItemChange: () => { },
-      inputValue: 'input-value',
-      onInputChange: () => { },
-    })
-    expect(wrapper.find('link').prop('items')[1].value).toBe(items[1].value)
-    expect(wrapper.find('link').prop('selectedItem')).toBe('item-2')
-  })
-
-  it('renders text input with correct data', () => {
-    let wrapper = wrap({
-      items,
-      selectedItem: 'item-2',
-      onItemChange: () => { },
-      inputValue: 'input-value',
-      onInputChange: () => { },
-    })
-    expect(wrapper.find('text').prop('embedded')).toBe(true)
-    expect(wrapper.find('text').prop('value')).toBe('input-value')
-  })
-})
-
-
-

+ 79 - 0
src/components/ui/Dropdowns/DropdownLink/DropdownLink.spec.tsx

@@ -0,0 +1,79 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import userEvent from '@testing-library/user-event'
+import { ThemeProps } from '@src/components/Theme'
+import DropdownLink from '.'
+
+const ITEMS = [
+  { label: 'Item 1', value: 'item1' },
+  { label: 'Item A2', value: 'item2' },
+  { label: 'Item A3', value: 'item3' },
+]
+
+describe('DropdownLink', () => {
+  it('renders select item label', () => {
+    render(<DropdownLink selectItemLabel="Select an item" items={ITEMS} />)
+    expect(TestUtils.select('DropdownLink__Label')?.textContent).toBe('Select an item')
+  })
+
+  it('renders no items label', () => {
+    render(<DropdownLink noItemsLabel="No items" items={[]} />)
+    expect(TestUtils.select('DropdownLink__Label')?.textContent).toBe('No items')
+  })
+
+  it('renders the selected item', () => {
+    render(<DropdownLink items={ITEMS} selectedItem={ITEMS[1].value} />)
+    expect(TestUtils.select('DropdownLink__Label')?.textContent).toBe(ITEMS[1].label)
+  })
+
+  it('fires selected item change', () => {
+    const onChange = jest.fn()
+    render(<DropdownLink items={ITEMS} onChange={onChange} />)
+    TestUtils.select('DropdownLink__LinkButton')?.click()
+    TestUtils.selectAll('DropdownLink__ListItem-')[1].click()
+    expect(onChange).toBeCalledWith(ITEMS[1])
+  })
+
+  it('can be searchable', () => {
+    render(<DropdownLink items={ITEMS} searchable />)
+    TestUtils.select('DropdownLink__LinkButton')?.click()
+    const input = TestUtils.selectContains('SearchInput__Input')!
+    userEvent.type(input, 'A')
+    const listItems = () => TestUtils.selectAll('DropdownLink__ListItem-')
+    expect(listItems()).toHaveLength(2)
+    expect(listItems()[1].textContent).toBe(ITEMS[2].label)
+    userEvent.clear(input)
+    userEvent.type(input, 'item3')
+    expect(listItems()).toHaveLength(1)
+    expect(listItems()[0].textContent).toBe(ITEMS[2].label)
+    expect(TestUtils.select('DropdownLink__EmptySearch')).toBeFalsy()
+    userEvent.clear(input)
+    userEvent.type(input, 'giberrish')
+    expect(listItems()).toHaveLength(0)
+    expect(TestUtils.select('DropdownLink__EmptySearch')).toBeTruthy()
+  })
+
+  it('highlights the highlighted item', () => {
+    render(<DropdownLink items={ITEMS} highlightedItem={ITEMS[1].value} />)
+    TestUtils.select('DropdownLink__LinkButton')?.click()
+    const noHighlightStyle = window.getComputedStyle(TestUtils.selectAll('DropdownLink__ListItemLabel')[0])
+    const highlightStyle = window.getComputedStyle(TestUtils.selectAll('DropdownLink__ListItemLabel')[1])
+    expect(highlightStyle.fontWeight).not.toBe(noHighlightStyle.fontWeight)
+    expect(highlightStyle.fontWeight).toBe(`${ThemeProps.fontWeights.medium}`)
+  })
+})

+ 2 - 2
src/components/ui/Dropdowns/DropdownLink/DropdownLink.tsx

@@ -213,9 +213,9 @@ class DropdownLink extends React.Component<Props, State> {
   getFilteredItems() {
   getFilteredItems() {
     const { items } = this.props
     const { items } = this.props
 
 
-    return items.filter(item => (typeof item.value === 'string'
+    return items.filter(item => ((typeof item.value === 'string'
       ? item.value.toLowerCase().indexOf(this.state.searchText.toLowerCase()) > -1
       ? item.value.toLowerCase().indexOf(this.state.searchText.toLowerCase()) > -1
-      : item.value === Number(this.state.searchText)
+      : item.value === Number(this.state.searchText))
         || item.label.toLowerCase().indexOf(this.state.searchText.toLowerCase()) > -1))
         || item.label.toLowerCase().indexOf(this.state.searchText.toLowerCase()) > -1))
   }
   }
 
 

+ 0 - 40
src/components/ui/Dropdowns/DropdownLink/test.tsx

@@ -1,40 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TestWrapper from '@src/utils/TestWrapper'
-import DropdownLink from '../DropdownLink'
-
-const wrap = props => new TestWrapper(shallow(<DropdownLink {...props} />), 'dropdownLink')
-
-describe('DropdownLink Component', () => {
-  it('renders with selectedItem', () => {
-    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,
-    })
-    expect(wrapper.findText('label')).toBe('Item 2')
-  })
-})
-
-
-

+ 47 - 0
src/components/ui/Dropdowns/NewItemDropdown/NewItemDropdown.spec.tsx

@@ -0,0 +1,47 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+
+import TestUtils from '@tests/TestUtils'
+import NewItemDropdown from '.'
+
+jest.mock('react-router-dom', () => ({
+  Link: 'div',
+}))
+
+describe('NewItemDropdown', () => {
+  it('renders new button', () => {
+    render(<NewItemDropdown onChange={() => { }} />)
+    expect(TestUtils.select('DropdownButton__Label')?.textContent).toBe('New')
+  })
+
+  it('fires change', () => {
+    const onChange = jest.fn()
+    render(<NewItemDropdown onChange={onChange} />)
+    TestUtils.select('DropdownButton__Wrapper')!.click()
+    TestUtils.selectAll('NewItemDropdown__ListItem')[2].click()
+    expect(onChange).toBeCalledWith(expect.objectContaining({ value: 'endpoint' }))
+  })
+
+  it('has list items with \'to\' property', () => {
+    render(<NewItemDropdown onChange={() => { }} />)
+    TestUtils.select('DropdownButton__Wrapper')!.click()
+    const listItems = TestUtils.selectAll('NewItemDropdown__ListItem')
+    expect(listItems[0].getAttribute('to')).toBe('/wizard/migration')
+    expect(listItems[1].getAttribute('to')).toBe('/wizard/replica')
+    expect(listItems[2].getAttribute('to')).toBe('#')
+  })
+})

+ 0 - 41
src/components/ui/Dropdowns/NewItemDropdown/test.tsx

@@ -1,41 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TW from '@src/utils/TestWrapper'
-import NewItemDropdown from '.'
-
-const wrap = props => new TW(shallow(<NewItemDropdown onChange={() => { }} {...props} />), 'newItemDropdown')
-
-describe('NewItemDropdown Component', () => {
-  it('opens list on click', () => {
-    let wrapper = wrap()
-    expect(wrapper.findPartialId('listItem').length).toBe(0)
-    wrapper.find('button').simulate('click')
-    expect(wrapper.findPartialId('listItem').length).toBe(3)
-  })
-
-  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')
-  })
-})
-
-
-

+ 89 - 0
src/components/ui/Dropdowns/NotificationDropdown/NotificationDropdown.spec.tsx

@@ -0,0 +1,89 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import NotificationDropdown from '@src/components/ui/Dropdowns/NotificationDropdown'
+
+import type { NotificationItemData } from '@src/@types/NotificationItem'
+import TestUtils from '@tests/TestUtils'
+
+jest.mock('react-router-dom', () => ({ Link: 'div' }))
+
+const ITEMS: NotificationItemData[] = [
+  {
+    id: '1',
+    type: 'migration',
+    name: 'Notification 1',
+    description: 'Description 1',
+    status: 'COMPLETED',
+  },
+  {
+    id: '2',
+    type: 'replica',
+    name: 'Notification 2',
+    description: 'Description 2',
+    status: 'ERROR',
+    unseen: true,
+  },
+  {
+    id: '3',
+    type: 'replica',
+    name: 'Notification 3',
+    description: 'Description 3',
+    status: 'RUNNING',
+  },
+]
+
+describe('NotificationDropdown', () => {
+  it('renders the bell icon', () => {
+    render(<NotificationDropdown items={[]} onClose={() => { }} />)
+    expect(TestUtils.select('NotificationDropdown__BellIcon')).toBeTruthy()
+  })
+
+  it('shows items on click', () => {
+    render(<NotificationDropdown items={ITEMS} onClose={() => { }} />)
+    TestUtils.select('NotificationDropdown__Icon')!.click()
+    const listItems = TestUtils.selectAll('NotificationDropdown__ListItem')
+    expect(listItems[0].getAttribute('to')).toBe(`/${ITEMS[0].type}s/${ITEMS[0].id}`)
+    expect(listItems[1].getAttribute('to')).toBe(`/${ITEMS[1].type}s/${ITEMS[1].id}`)
+    expect(listItems[2].getAttribute('to')).toBe(`/${ITEMS[2].type}s/${ITEMS[2].id}/executions`)
+    expect(TestUtils.select('NotificationDropdown__ItemReplicaBadge', listItems[2])!.textContent).toBe('RE')
+    expect(TestUtils.select('NotificationDropdown__ItemTitle', listItems[2])!.textContent).toBe(ITEMS[2].name)
+    expect(TestUtils.select('NotificationDropdown__ItemDescription', listItems[2])!.textContent).toBe(ITEMS[2].description)
+  })
+
+  it('fires onClose on item click', () => {
+    const onClose = jest.fn()
+    render(<NotificationDropdown items={ITEMS} onClose={onClose} />)
+    TestUtils.select('NotificationDropdown__Icon')!.click()
+    TestUtils.selectAll('NotificationDropdown__ListItem')[1].click()
+    expect(onClose).toHaveBeenCalled()
+  })
+
+  it('renders unseed badge', () => {
+    render(<NotificationDropdown items={ITEMS} onClose={() => { }} />)
+    TestUtils.select('NotificationDropdown__Icon')!.click()
+    const listItems = TestUtils.selectAll('NotificationDropdown__ListItem')
+    expect(TestUtils.select('NotificationDropdown__Badge', listItems[0])).toBeFalsy()
+    expect(TestUtils.select('NotificationDropdown__Badge', listItems[1])).toBeTruthy()
+  })
+
+  it('renders loading when item is RUNNING', () => {
+    const { rerender } = render(<NotificationDropdown items={ITEMS} onClose={() => { }} />)
+    expect(TestUtils.select('NotificationDropdown__Loading')).toBeTruthy()
+    rerender(<NotificationDropdown items={ITEMS.map(item => ({ ...item, status: 'COMPLETED' }))} onClose={() => { }} />)
+    expect(TestUtils.select('NotificationDropdown__Loading')).toBeFalsy()
+  })
+})

+ 0 - 99
src/components/ui/Dropdowns/NotificationDropdown/test.tsx

@@ -1,99 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import sinon from 'sinon'
-
-import type { NotificationItemData } from '@src/@types/NotificationItem'
-import TW from '@src/utils/TestWrapper'
-import NotificationDropdown from '.'
-import type { Props } from '.'
-
-const wrap = (props: Props) => new TW(shallow(
-  <NotificationDropdown {...props} />
-), 'notificationDropdown')
-
-let items: NotificationItemData[] = [
-  {
-    id: '1',
-    name: 'notif-1',
-    description: 'desc-1',
-    type: 'replica',
-    status: 'COMPLETED',
-    unseen: false,
-  },
-  {
-    id: '2',
-    name: 'notif-2',
-    description: 'desc-2',
-    type: 'migration',
-    status: 'RUNNING',
-    unseen: true,
-  },
-  {
-    id: '3',
-    name: 'notif-3',
-    description: 'desc-3',
-    type: 'replica',
-    status: 'ERROR',
-    unseen: false,
-  },
-]
-
-describe('NotificationDropdown Component', () => {
-  it('renders no items message on click', () => {
-    let wrapper = wrap({ onClose: () => { }, items: [] })
-    expect(wrapper.find('noItems').length).toBe(0)
-    wrapper.find('button').simulate('click')
-    expect(wrapper.find('noItems').length).toBe(1)
-    expect(wrapper.find('bell-badge').length).toBe(0)
-    expect(wrapper.find('bell-loading').length).toBe(0)
-  })
-
-  it('renders items correctly', () => {
-    let wrapper = wrap({ items, onClose: () => { } })
-    wrapper.find('button').simulate('click')
-    expect(wrapper.find('bell-badge').length).toBe(1)
-    expect(wrapper.find('bell-loading').length).toBe(1)
-
-    items.forEach(item => {
-      expect(wrapper.find(`${item.id}-status`).prop('status')).toBe(item.status)
-      expect(wrapper.findText(`${item.id}-type`)).toBe(item.type === 'replica' ? 'RE' : 'MI')
-      expect(wrapper.findText(`${item.id}-name`)).toBe(item.name)
-      expect(wrapper.findText(`${item.id}-description`)).toBe(item.description)
-      expect(wrapper.find(`${item.id}-badge`).length).toBe(item.unseen ? 1 : 0)
-    })
-  })
-
-  it('renders button bell badge', () => {
-    let wrapper = wrap({ items: items.map(i => { return { ...i, unseen: false } }), onClose: () => { } })
-    expect(wrapper.find('bell-badge').length).toBe(0)
-    expect(wrapper.find('bell-loading').length).toBe(1)
-    wrapper = wrap({ items: items.map(i => { return { ...i, status: 'COMPLETED' } }), onClose: () => { } })
-    expect(wrapper.find('bell-badge').length).toBe(1)
-    expect(wrapper.find('bell-loading').length).toBe(0)
-  })
-
-  it('dispatches onClose', () => {
-    let onClose = sinon.spy()
-    let wrapper = wrap({ items, onClose })
-    wrapper.find('button').simulate('click')
-    wrapper.find(`${items[0].id}-item`).simulate('click')
-    expect(onClose.calledOnce).toBe(true)
-  })
-})
-
-
-

+ 57 - 0
src/components/ui/Dropdowns/UserDropdown/UserDropdown.spec.tsx

@@ -0,0 +1,57 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import { User } from '@src/@types/User'
+import UserDropdown from '.'
+
+jest.mock('react-router-dom', () => ({ Link: 'div' }))
+
+const USER: User = {
+  id: 'user-id',
+  name: 'User Name',
+  email: 'email@email.test',
+  project: {
+    id: 'project-id',
+    name: 'Project Name',
+  },
+}
+
+describe('UserDropdown', () => {
+  it('renders no user', () => {
+    render(<UserDropdown user={null} onItemClick={() => { }} />)
+    TestUtils.select('UserDropdown__Icon')?.click()
+    expect(TestUtils.select('UserDropdown__Label')?.textContent).toBe('No signed in user')
+  })
+
+  it('renders user menu', () => {
+    render(<UserDropdown user={USER} onItemClick={() => { }} />)
+    TestUtils.select('UserDropdown__Icon')?.click()
+    expect(TestUtils.select('UserDropdown__Username')?.textContent).toBe(USER.name)
+    expect(TestUtils.select('UserDropdown__Email')?.textContent).toBe(USER.email)
+    const listItems = TestUtils.selectAll('UserDropdown__ListItem')
+    expect(listItems).toHaveLength(3)
+    expect(listItems[0].textContent).toBe('About Coriolis')
+  })
+
+  it('fires item click', () => {
+    const onItemClick = jest.fn()
+    render(<UserDropdown user={USER} onItemClick={onItemClick} />)
+    TestUtils.select('UserDropdown__Icon')?.click()
+    TestUtils.selectAll('UserDropdown__Label')[2].click()
+    expect(onItemClick).toHaveBeenCalledWith(expect.objectContaining({ value: 'signout' }))
+  })
+})

+ 0 - 52
src/components/ui/Dropdowns/UserDropdown/test.tsx

@@ -1,52 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TW from '@src/utils/TestWrapper'
-import UserDropdown from '.'
-
-const wrap = props => new TW(shallow(
-
-  <UserDropdown {...props} />
-), 'userDropdown')
-
-let user = { name: 'User name', email: 'email@email.com' }
-
-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.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.find('button').simulate('click')
-    wrapper.find('label-signout').simulate('click')
-    expect(onItemClick.args[0][0].value).toBe('signout')
-  })
-})
-
-
-

+ 175 - 0
src/components/ui/FieldInput/FieldInput.spec.tsx

@@ -0,0 +1,175 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import userEvent from '@testing-library/user-event'
+
+import FieldInput from '.'
+
+jest.mock('@src/plugins/default/ContentPlugin', () => jest.fn(() => null))
+
+describe('FieldInput', () => {
+  it('renders field label and description', () => {
+    render(
+      <FieldInput
+        name="Field Name"
+        label="Field Label"
+        description="Field Description"
+      />,
+    )
+    expect(TestUtils.select('FieldInput__LabelText')?.textContent).toBe('Field Label')
+    expect(TestUtils.select('InfoIcon__Wrapper')?.getAttribute('data-tip')).toBe('Field Description')
+  })
+
+  it('renders string field', () => {
+    render(
+      <FieldInput
+        name="Field Name"
+        type="string"
+        value="Field Value"
+      />,
+    )
+    expect(TestUtils.selectInput('TextInput__Input')!.value).toBe('Field Value')
+  })
+
+  it('renders string field with enumerator', () => {
+    const { rerender } = render(
+      <FieldInput
+        name="Field Name"
+        type="string"
+        value="Field Value"
+        enum={['foo', 'bar']}
+      />,
+    )
+    TestUtils.select('DropdownButton__Wrapper')?.click()
+    expect(TestUtils.selectAll('Dropdown__ListItem-')).toHaveLength(2)
+    expect(TestUtils.select('Dropdown__ListItem-')?.textContent).toBe('foo')
+    rerender(
+      <FieldInput
+        name="Field Name"
+        type="string"
+        value="Field Value"
+        enum={['foo', 'bar', 'baz', 'qux', 'quux', 'corge', 'grault', 'garply', 'waldo', 'fred', 'plugh', 'xyzzy', 'thud']}
+      />,
+    )
+    expect(TestUtils.select('AutocompleteDropdown__Wrapper')).toBeTruthy()
+    userEvent.type(TestUtils.select('TextInput__Input')!, 'ba')
+    expect(TestUtils.selectAll('AutocompleteDropdown__ListItem-')).toHaveLength(2)
+    expect(TestUtils.selectAll('AutocompleteDropdown__ListItem-')[1].textContent).toBe('baz')
+  })
+
+  it('renders text input if empty enums array', () => {
+    render(
+      <FieldInput
+        name="Field Name"
+        type="string"
+        value="Field Value"
+        enum={[]}
+      />,
+    )
+    expect(TestUtils.selectInput('TextInput__Input')!.value).toBe('Field Value')
+  })
+
+  it('renders text area', () => {
+    render(
+      <FieldInput
+        name="Field Name"
+        type="string"
+        value="Field Value"
+        useTextArea
+      />,
+    )
+    expect(document.querySelector('textarea')?.value).toBe('Field Value')
+  })
+
+  it('renders file input', () => {
+    render(
+      <FieldInput
+        name="Field Name"
+        useFile
+        type="string"
+        value="Field Value"
+      />,
+    )
+    expect(document.querySelector('input')?.getAttribute('type')).toBe('file')
+  })
+
+  it('renders integer input', () => {
+    const { rerender } = render(
+      <FieldInput
+        name="Field Name"
+        type="integer"
+        minimum={0}
+        maximum={100}
+        value={10}
+      />,
+    )
+    expect(TestUtils.selectInput('Stepper__Input')!.value).toBe('10')
+
+    rerender(
+      <FieldInput
+        name="Field Name"
+        type="integer"
+        minimum={1}
+        maximum={8}
+        value={5}
+      />,
+    )
+    TestUtils.select('DropdownButton__Wrapper')?.click()
+    expect(TestUtils.selectAll('Dropdown__ListItem-')).toHaveLength(8)
+    expect(TestUtils.selectAll('Dropdown__ListItem-')[1].textContent).toBe('2')
+    expect(TestUtils.select('DropdownButton__Label')?.textContent).toBe('5')
+  })
+
+  it('renders radio input', () => {
+    render(
+      <FieldInput
+        name="Field Name"
+        type="radio"
+        value
+      />,
+    )
+    expect(TestUtils.select('RadioInput__Input')?.hasAttribute('checked')).toBeTruthy()
+  })
+
+  it('renders array dropdown', () => {
+    render(
+      <FieldInput
+        name="Field Name"
+        type="array"
+        enum={['foo', 'bar', 'baz']}
+        value={['bar', 'baz']}
+      />,
+    )
+    expect(TestUtils.select('DropdownButton__Label')?.textContent).toBe('bar, baz')
+    TestUtils.select('DropdownButton__Wrapper')?.click()
+  })
+
+  it('renders object field', () => {
+    render(
+      <FieldInput
+        name="Field Name"
+        type="object"
+        valueCallback={field => `value-${field.name}`}
+        properties={[{ name: 'Prop 1', type: 'string' }, { name: 'Prop 2', type: 'string' }]}
+      />,
+    )
+    const rows = TestUtils.selectAll('PropertiesTable__Row')
+    expect(rows).toHaveLength(2)
+    expect(TestUtils.select('PropertiesTable__Column', rows[1])?.textContent).toBe('Prop 2')
+    expect(TestUtils.selectInput('TextInput__Input', rows[1])!.value).toBe('value-Prop 2')
+  })
+})

+ 33 - 0
src/components/ui/FileInput/FileInput.spec.tsx

@@ -0,0 +1,33 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import { waitFor } from '@testing-library/dom'
+import userEvent from '@testing-library/user-event'
+import FileInput from '@src/components/ui/FileInput'
+import TestUtils from '@tests/TestUtils'
+
+describe('FileInput', () => {
+  it('uploads file', async () => {
+    const onUpload = jest.fn()
+    render(<FileInput onUpload={onUpload} />)
+    userEvent.upload(
+      TestUtils.select('FileInput__FakeFileInput')!,
+      [new File(['test-content'], 'test.txt', { type: 'text/plain' })],
+    )
+    await waitFor(() => expect(onUpload).toHaveBeenCalledWith('test-content'))
+    expect(TestUtils.select('FileInput__FileName')?.textContent).toBe('test.txt')
+  })
+})

+ 1 - 1
src/components/ui/FileInput/FileInput.tsx

@@ -113,8 +113,8 @@ class FileInput extends React.Component<Props, State> {
       return
       return
     }
     }
     const fileName = files[0].name
     const fileName = files[0].name
-    const content = await FileUtils.readTextFromFirstFile(files)
     this.setState({ fileName })
     this.setState({ fileName })
+    const content = await FileUtils.readTextFromFirstFile(files)
     if (this.props.onUpload) {
     if (this.props.onUpload) {
       this.props.onUpload(content)
       this.props.onUpload(content)
     }
     }

+ 7 - 12
src/components/ui/CopyButton/test.tsx → src/components/ui/HorizontalLoading/HorizontalLoading.spec.tsx

@@ -1,5 +1,5 @@
 /*
 /*
-Copyright (C) 2017  Cloudbase Solutions SRL
+Copyright (C) 2021  Cloudbase Solutions SRL
 This program is free software: you can redistribute it and/or modify
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
 it under the terms of the GNU Affero General Public License as
 published by the Free Software Foundation, either version 3 of the
 published by the Free Software Foundation, either version 3 of the
@@ -14,17 +14,12 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import { render } from '@testing-library/react'
 import { render } from '@testing-library/react'
-import CopyButton from './CopyButton'
+import HorizontalLoading from '@src/components/ui/HorizontalLoading'
+import TestUtils from '@tests/TestUtils'
 
 
-const wrap = props => shallow(<CopyButton {...props} />).dive()
-
-describe('CopyButton Component', () => {
-  it('should receive the props', () => {
-    let onClick = () => {}
-    let span = wrap({ onClick })
-    expect(span.prop('onClick')).toBe(onClick)
+describe('HorizontalLoading', () => {
+  it('renders', () => {
+    render(<HorizontalLoading />)
+    expect(TestUtils.select('HorizontalLoading__Wrapper')).toBeTruthy()
   })
   })
 })
 })
-
-
-

+ 33 - 0
src/components/ui/InfoIcon/InfoIcon.spec.tsx

@@ -0,0 +1,33 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import InfoIcon from '@src/components/ui/InfoIcon'
+import TestUtils from '@tests/TestUtils'
+
+describe('InfoIcon', () => {
+  it('renders with data tip and apropriate icons', () => {
+    const { rerender } = render(<InfoIcon text="info text" />)
+    expect(TestUtils.select('InfoIcon__Wrapper')?.getAttribute('data-tip')).toBe('info text')
+    const style = () => window.getComputedStyle(TestUtils.select('InfoIcon__Wrapper')!)
+    expect(style().backgroundImage).toBe('url(question.svg)')
+
+    rerender(<InfoIcon text="info text" warning />)
+    expect(style().backgroundImage).toBe('url(warning.svg)')
+
+    rerender(<InfoIcon text="info text" filled />)
+    expect(style().backgroundImage).toBe('url(question-filled.svg)')
+  })
+})

+ 137 - 0
src/components/ui/Lists/FilterList/FilterList.spec.tsx

@@ -0,0 +1,137 @@
+/*
+Copyright (C) 2021  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 styled from 'styled-components'
+import { render } from '@testing-library/react'
+import FilterList from '@src/components/ui/Lists/FilterList'
+import TestUtils from '@tests/TestUtils'
+import userEvent from '@testing-library/user-event'
+import { ItemComponentProps } from '@src/components/ui/Lists/MainList'
+
+jest.mock('@src/utils/Config', () => ({
+  config: { mainListItemsPerPage: 2 },
+}))
+
+const ITEMS = [
+  { id: 'item-1', label: 'Item 1' },
+  { id: 'item-2', label: 'Item 2' },
+  { id: 'item-3', label: 'Item 3' },
+  { id: 'item-3-a', label: 'Item 3-a' },
+]
+
+const FILTER_ITEMS = [
+  { label: 'All', value: 'all' },
+  { label: 'Items 1', value: 'item-1' },
+  { label: 'Items 2', value: 'item-2' },
+  { label: 'Items 3', value: 'item-3' },
+]
+
+const itemFilterFunction = (item: any, filterStatus?: string | null, filterText?: string) => {
+  if (
+    (filterStatus !== 'all' && item.id.indexOf(filterStatus) === -1)
+    || (item.label.indexOf(filterText) === -1)
+  ) {
+    return false
+  }
+
+  return true
+}
+
+const MainListItem = styled.div``
+
+const ItemComponent = (props: ItemComponentProps) => (
+  <MainListItem
+    key={props.key}
+    onClick={() => props.onClick()}
+  >
+    <input
+      type="checkbox"
+      defaultChecked={props.selected}
+      onChange={e => { props.onSelectedChange(e.currentTarget.checked) }}
+    />
+    {props.item.label}
+  </MainListItem>
+)
+
+const FilterListWrap = (options?: {
+  onItemClick?: () => void
+  onSelectedItemsChange?: (items: any[]) => void
+}) => (
+  <FilterList
+    items={ITEMS}
+    filterItems={FILTER_ITEMS}
+    itemFilterFunction={itemFilterFunction}
+    loading={false}
+    onReloadButtonClick={() => { }}
+    onItemClick={options?.onItemClick || (() => { })}
+    selectionLabel="test item"
+    renderItemComponent={ItemComponent}
+    onSelectedItemsChange={options?.onSelectedItemsChange || (() => { })}
+  />
+)
+
+describe('FilterList', () => {
+  it('renders all elements', () => {
+    render(FilterListWrap())
+    const filterItems = TestUtils.selectAll('MainListFilter__FilterItem')
+    expect(filterItems).toHaveLength(FILTER_ITEMS.length)
+    expect(filterItems[2].textContent).toBe(FILTER_ITEMS[2].label)
+    const listItems = TestUtils.selectAll('FilterListspec__MainListItem-')
+    expect(listItems).toHaveLength(2)
+    expect(listItems[1].textContent).toBe(ITEMS[1].label)
+    expect(TestUtils.select('Pagination__PageNumber')?.textContent).toBe('1 of 2')
+  })
+
+  it('filters items', () => {
+    render(FilterListWrap())
+    TestUtils.selectAll('MainListFilter__FilterItem')[3].click()
+    const listItems = () => TestUtils.selectAll('FilterListspec__MainListItem-')
+    expect(listItems()).toHaveLength(2)
+    expect(listItems()[1].textContent).toBe(ITEMS[3].label)
+
+    userEvent.type(TestUtils.selectInput('TextInput__Input', TestUtils.select('SearchInput__Wrapper')!)!, 'Item 3-a')
+    expect(listItems()).toHaveLength(1)
+    expect(listItems()[0].textContent).toBe(ITEMS[3].label)
+
+    userEvent.type(TestUtils.selectInput('TextInput__Input', TestUtils.select('SearchInput__Wrapper')!)!, 'gibberish')
+    expect(TestUtils.select('MainList__NoResults')).toBeTruthy()
+  })
+
+  it('goes to next page', () => {
+    render(FilterListWrap())
+    TestUtils.select('Pagination__PageNext')?.click()
+
+    expect(TestUtils.select('Pagination__PageNumber')?.textContent).toBe('2 of 2')
+    expect(TestUtils.selectAll('FilterListspec__MainListItem-')[1].textContent).toBe(ITEMS[3].label)
+  })
+
+  it('fires item click', () => {
+    const onItemClick = jest.fn()
+    render(FilterListWrap({ onItemClick }))
+    TestUtils.selectAll('FilterListspec__MainListItem-')[1].click()
+    expect(onItemClick).toHaveBeenCalledWith(ITEMS[1])
+  })
+
+  it('selects items', () => {
+    const onSelectedItemsChange = jest.fn()
+    render(FilterListWrap({ onSelectedItemsChange }))
+    const checkbox = TestUtils.selectAll('FilterListspec__MainListItem-')[1].querySelector('input') as HTMLInputElement
+    expect(checkbox.checked).toBe(false)
+    checkbox.click()
+    expect(checkbox.checked).toBe(true)
+    expect(TestUtils.select('MainListFilter__SelectionText')?.textContent).toBe('1 of 4\u00a0test item(s) selected')
+    expect(onSelectedItemsChange).toHaveBeenCalledWith([ITEMS[1]])
+  })
+})

+ 9 - 9
src/components/ui/Lists/FilterList/FilterList.tsx

@@ -127,20 +127,20 @@ class FilterList extends React.Component<Props, State> {
       })
       })
 
 
       const selectAllSelected = selectedItems.length > 0 && selectedItems.length === items.length
       const selectAllSelected = selectedItems.length > 0 && selectedItems.length === items.length
-      this.setState({
+      return {
         selectedItems,
         selectedItems,
         selectAllSelected,
         selectAllSelected,
         filterStatus: item.value,
         filterStatus: item.value,
         items,
         items,
         currentPage: 1,
         currentPage: 1,
-      }, () => {
-        if (this.props.onSelectedItemsChange) {
-          this.props.onSelectedItemsChange(selectedItems)
-        }
-        if (this.props.onPaginatedItemsChange) {
-          this.props.onPaginatedItemsChange(this.paginatedItems)
-        }
-      })
+      }
+    }, () => {
+      if (this.props.onSelectedItemsChange) {
+        this.props.onSelectedItemsChange(this.state.selectedItems)
+      }
+      if (this.props.onPaginatedItemsChange) {
+        this.props.onPaginatedItemsChange(this.paginatedItems)
+      }
     })
     })
   }
   }
 
 

+ 0 - 75
src/components/ui/Lists/FilterList/test.tsx

@@ -1,75 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import TW from '@src/utils/TestWrapper'
-import FilterList from '.'
-
-const wrap = props => new TW(shallow(
-
-  <FilterList {...props} />
-), 'filterList')
-
-let items = [
-  { id: 'item-1', label: 'Item 1' },
-  { id: 'item-2', label: 'Item 2' },
-  { id: 'item-3', label: 'Item 3' },
-  { id: 'item-3-a', label: 'Item 3-a' },
-]
-
-let filterItems = [
-  { label: 'All', value: 'all' },
-  { label: 'Items 1', value: 'item-1' },
-  { label: 'Items 2', value: 'item-2' },
-  { label: 'Items 3', value: 'item-3' },
-]
-
-let actions = [{ label: 'action', value: 'action' }]
-
-let itemFilterFunction = (item, filterStatus, filterText) => {
-  if (
-    (filterStatus !== 'all' && item.id.indexOf(filterStatus) === -1) ||
-    (item.label.toLowerCase().indexOf(filterText) === -1)
-  ) {
-    return false
-  }
-
-  return true
-}
-
-describe('FilterList Component', () => {
-  it('renders with all items', () => {
-    let wrapper = wrap({ items, filterItems, itemFilterFunction, actions })
-    expect(wrapper.find('mainList').prop('items').length).toBe(4)
-  })
-
-  it('handles filter item click', () => {
-    let wrapper = wrap({ items, filterItems, itemFilterFunction, actions })
-    wrapper.find('filter').simulate('filterItemClick', { ...filterItems[2] })
-    expect(wrapper.find('mainList').prop('items').length).toBe(1)
-    expect(wrapper.find('mainList').prop('items')[0].id).toBe('item-2')
-  })
-
-  it('handles search change', () => {
-    let wrapper = wrap({ items, filterItems, itemFilterFunction, actions })
-    wrapper.find('filter').simulate('searchChange', 'item 3')
-    expect(wrapper.find('mainList').prop('items').length).toBe(2)
-    expect(wrapper.find('mainList').prop('items')[0].id).toBe('item-3')
-    expect(wrapper.find('mainList').prop('items')[1].id).toBe('item-3-a')
-  })
-})
-
-
-

+ 110 - 0
src/components/ui/Lists/MainList/MainList.spec.tsx

@@ -0,0 +1,110 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import MainList, { ItemComponentProps } from '@src/components/ui/Lists/MainList'
+import styled from 'styled-components'
+import TestUtils from '@tests/TestUtils'
+
+const ITEMS: any[] = [
+  { id: 'item-1', label: 'Item 1' },
+  { id: 'item-2', label: 'Item 2' },
+  { id: 'item-3', label: 'Item 3' },
+  { id: 'item-3-a', label: 'Item 3-a' },
+]
+const MainListItem = styled.div``
+
+const ItemComponent = (props: ItemComponentProps) => (
+  <MainListItem
+    key={props.key}
+    onClick={() => props.onClick()}
+  >
+    <input
+      type="checkbox"
+      defaultChecked={props.selected}
+      onChange={e => { props.onSelectedChange(e.currentTarget.checked) }}
+    />
+    {props.item.label}
+  </MainListItem>
+)
+
+const MainListWrap = (options?: {
+  onItemClick?: () => void
+  onSelectedItemsChange?: (items: any[]) => void
+  onEmptyListButtonClick?: () => void,
+  loading?: boolean
+  showEmptyList?: boolean
+}) => (
+  <MainList
+    items={ITEMS}
+    selectedItems={[ITEMS[2], ITEMS[3]]}
+    loading={Boolean(options?.loading)}
+    onItemClick={options?.onItemClick || (() => {})}
+    onSelectedChange={options?.onSelectedItemsChange || (() => {})}
+    renderItemComponent={ItemComponent}
+    showEmptyList={Boolean(options?.showEmptyList)}
+    emptyListButtonLabel="New item"
+    onEmptyListButtonClick={options?.onEmptyListButtonClick || (() => {})}
+  />
+)
+describe('MainList', () => {
+  it('renders items', () => {
+    render(<MainListWrap />)
+    const items = TestUtils.selectAll('MainListspec__MainListItem-')
+    expect(items).toHaveLength(ITEMS.length)
+    expect(items[0].querySelector('input')!.checked).toBe(false)
+    expect(items[1].querySelector('input')!.checked).toBe(false)
+    expect(items[2].querySelector('input')!.checked).toBe(true)
+    expect(items[3].querySelector('input')!.checked).toBe(true)
+  })
+
+  it('fires item click', () => {
+    const onItemClick = jest.fn()
+    render(<MainListWrap onItemClick={onItemClick} />)
+    TestUtils.selectAll('MainListspec__MainListItem-')[1].click()
+    expect(onItemClick).toHaveBeenCalledWith(ITEMS[1])
+  })
+
+  it('fires selection change', () => {
+    const onSelectedItemsChange = jest.fn()
+    render(<MainListWrap onSelectedItemsChange={onSelectedItemsChange} />)
+    TestUtils.selectAll('MainListspec__MainListItem-')[1].querySelector('input')!.click()
+    expect(onSelectedItemsChange).toHaveBeenCalledWith(ITEMS[1], true)
+    TestUtils.selectAll('MainListspec__MainListItem-')[1].querySelector('input')!.click()
+    expect(onSelectedItemsChange).toHaveBeenCalledWith(ITEMS[1], false)
+  })
+
+  it('shows loading', () => {
+    const { rerender } = render(<MainListWrap loading />)
+    expect(TestUtils.select('MainList__LoadingText')).toBeTruthy()
+    rerender(<MainListWrap />)
+    expect(TestUtils.select('MainList__LoadingText')).toBeFalsy()
+  })
+
+  it('shows empty list', () => {
+    const { rerender } = render(<MainListWrap />)
+    expect(TestUtils.select('MainList__EmptyList')).toBeFalsy()
+
+    const onEmptyListButtonClick = jest.fn()
+    rerender(<MainListWrap showEmptyList onEmptyListButtonClick={onEmptyListButtonClick} />)
+    expect(TestUtils.select('MainList__EmptyList')).toBeTruthy()
+
+    const button = TestUtils.select('MainList__EmptyList')?.querySelector('button')
+    expect(button).toBeTruthy()
+    expect(button!.textContent).toBe('New item')
+    button?.click()
+    expect(onEmptyListButtonClick).toHaveBeenCalled()
+  })
+})

+ 0 - 90
src/components/ui/Lists/MainList/test.tsx

@@ -1,90 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TW from '@src/utils/TestWrapper'
-import MainList from '.'
-
-const wrap = props => new TW(shallow(
-
-  <MainList {...props} />
-), 'mainList')
-
-let items = [
-  { id: 'item-1', label: 'Item 1' },
-  { id: 'item-2', label: 'Item 2' },
-  { id: 'item-3', label: 'Item 3' },
-  { id: 'item-3-a', label: 'Item 3-a' },
-]
-
-let selectedItems = [{ ...items[1] }, { ...items[2] }]
-
-let renderItemComponent = options => <div {...options}>{options.item.label}</div>
-
-describe('MainList Component', () => {
-  it('renders all items', () => {
-    let wrapper = wrap({ items, selectedItems, renderItemComponent })
-    items.forEach(item => {
-      expect(wrapper.findText(`item-${item.id}`, true)).toBe(item.label)
-    })
-    expect(wrapper.find('loadingStatus').length).toBe(0)
-  })
-
-  it('renders loading', () => {
-    let wrapper = wrap({ items, selectedItems, renderItemComponent, loading: true })
-    expect(wrapper.find('loadingStatus').length).toBe(1)
-  })
-
-  it('renders selected items', () => {
-    let wrapper = wrap({ items, selectedItems, renderItemComponent })
-    expect(wrapper.find('item-item-1').prop('selected')).toBe(false)
-    expect(wrapper.find('item-item-2').prop('selected')).toBe(true)
-    expect(wrapper.find('item-item-3').prop('selected')).toBe(true)
-    expect(wrapper.find('item-item-3-a').prop('selected')).toBe(false)
-  })
-
-  it('renders empty list', () => {
-    let wrapper = wrap({
-      items,
-      selectedItems,
-      renderItemComponent,
-      showEmptyList: true,
-      emptyListMessage: 'empty-list-message',
-      emptyListExtraMessage: 'empty-list-extra-message',
-      emptyListButtonLabel: 'empty-list-button-label',
-    })
-
-    expect(wrapper.findText('emptyMessage')).toBe('empty-list-message')
-    expect(wrapper.find('emptyListButton').shallow.dive().dive().text()).toBe('empty-list-button-label')
-  })
-
-  it('dispaches empty list button click', () => {
-    let onEmptyListButtonClick = sinon.spy()
-    let wrapper = wrap({
-      items,
-      selectedItems,
-      renderItemComponent,
-      showEmptyList: true,
-      onEmptyListButtonClick,
-      emptyListButtonLabel: 'New Item',
-    })
-    wrapper.find('emptyListButton').simulate('click')
-    expect(onEmptyListButtonClick.calledOnce).toBe(true)
-  })
-})
-
-
-

+ 131 - 0
src/components/ui/Lists/MainListFilter/MainListFilter.spec.tsx

@@ -0,0 +1,131 @@
+/*
+Copyright (C) 2021  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, { useState } from 'react'
+import { render } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import MainListFilter from '@src/components/ui/Lists/MainListFilter'
+import { Action } from '@src/components/ui/Dropdowns/ActionDropdown'
+import TestUtils from '@tests/TestUtils'
+import { ThemePalette } from '@src/components/Theme'
+
+const FILTER_ITEMS = [
+  { label: 'All', value: 'all' },
+  { label: 'Items 1', value: 'item-1' },
+  { label: 'Items 2', value: 'item-2' },
+  { label: 'Items 3', value: 'item-3' },
+]
+
+const ACTIONS: Action[] = [
+  {
+    label: 'Action 1',
+    title: 'Action 1 Description',
+    action: jest.fn(),
+  },
+  {
+    label: 'Action 2',
+    disabled: true,
+    action: jest.fn(),
+  },
+]
+
+const MainListFilterWrapper = (props?: {
+  onFilterItemClick?: (item: any) => void,
+  onReloadButtonClick?: () => void,
+  onSearchChange?: (value: string) => void,
+  onSelectAllChange?: (checked: boolean) => void,
+}) => {
+  const [selectAllSelected, setSelectAllSelected] = useState(false)
+  return (
+    <MainListFilter
+      onFilterItemClick={props?.onFilterItemClick || (() => { })}
+      onReloadButtonClick={props?.onReloadButtonClick || (() => { })}
+      onSearchChange={props?.onSearchChange || (() => { })}
+      onSelectAllChange={checked => {
+        setSelectAllSelected(checked)
+        if (props?.onSelectAllChange) {
+          props.onSelectAllChange(checked)
+        }
+      }}
+      selectedValue="item-2"
+      selectionInfo={{ total: 3, selected: 1, label: 'test item' }}
+      selectAllSelected={selectAllSelected}
+      items={FILTER_ITEMS}
+      dropdownActions={ACTIONS}
+      searchValue="test"
+    />
+  )
+}
+
+describe('MainListFilter', () => {
+  it('renders all basic elements', () => {
+    render(<MainListFilterWrapper />)
+    const items = TestUtils.selectAll('MainListFilter__FilterItem-')
+    expect(items).toHaveLength(FILTER_ITEMS.length)
+    expect(items[2].textContent).toBe(FILTER_ITEMS[2].label)
+    expect(TestUtils.select('SearchInput__Wrapper')?.querySelector('input')?.value).toBe('test')
+    expect(TestUtils.select('MainListFilter__SelectionText')?.textContent).toBe('1 of 3\u00a0test item(s) selected')
+  })
+
+  it('renders actions', () => {
+    render(<MainListFilterWrapper />)
+    TestUtils.select('DropdownButton__Wrapper')?.click()
+    const actions = TestUtils.selectAll('ActionDropdown__ListItem-')
+    expect(actions).toHaveLength(ACTIONS.length)
+    expect(actions[0].textContent).toBe(ACTIONS[0].label)
+    expect(actions[0].hasAttribute('disabled')).toBeFalsy()
+    expect(actions[1].hasAttribute('disabled')).toBeTruthy()
+    actions[0].click()
+    actions[1].click()
+    expect(ACTIONS[0].action).toHaveBeenCalled()
+    expect(ACTIONS[1].action).not.toHaveBeenCalled()
+  })
+
+  it('fires filter item click', () => {
+    const onFilterItemClick = jest.fn()
+    render(<MainListFilterWrapper onFilterItemClick={onFilterItemClick} />)
+    TestUtils.selectAll('MainListFilter__FilterItem-')[1].click()
+    expect(onFilterItemClick).toHaveBeenCalledWith(FILTER_ITEMS[1])
+  })
+
+  it('has select all change', () => {
+    const onSelectAllChange = jest.fn()
+    render(<MainListFilterWrapper onSelectAllChange={onSelectAllChange} />)
+
+    const checkbox = TestUtils.select('Checkbox__Wrapper')!
+    const style = () => window.getComputedStyle(checkbox)
+    expect(TestUtils.rgbToHex(style().backgroundColor)).toBe('white')
+    checkbox.click()
+    expect(TestUtils.rgbToHex(style().backgroundColor)).toBe(ThemePalette.primary)
+
+    expect(onSelectAllChange).toHaveBeenCalledWith(true)
+    checkbox.click()
+    expect(onSelectAllChange).toHaveBeenCalledWith(false)
+    expect(TestUtils.rgbToHex(style().backgroundColor)).toBe('white')
+  })
+
+  it('fires reload button click', () => {
+    const onReloadButtonClick = jest.fn()
+    render(<MainListFilterWrapper onReloadButtonClick={onReloadButtonClick} />)
+    TestUtils.select('ReloadButton__Wrapper')!.click()
+    expect(onReloadButtonClick).toHaveBeenCalled()
+  })
+
+  it('fires search change', () => {
+    const onSearchChange = jest.fn()
+    render(<MainListFilterWrapper onSearchChange={onSearchChange} />)
+    userEvent.type(TestUtils.select('SearchInput__Wrapper')?.querySelector('input')!, 'test2')
+    expect(onSearchChange).toHaveBeenCalledWith('test2')
+  })
+})

+ 0 - 64
src/components/ui/Lists/MainListFilter/test.tsx

@@ -1,64 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TestWrapper from '@src/utils/TestWrapper'
-import MainListFilter from '.'
-
-const wrap = props => new TestWrapper(shallow(
-
-  <MainListFilter {...props} />
-), 'mainListFilter')
-
-let items = [
-  { label: 'Item 1', value: 'item-1' },
-  { label: 'Item 2', value: 'item-2' },
-  { label: 'Item 3', value: 'item-3' },
-]
-
-let selectionInfo = { selected: 2, total: 7, label: 'items' }
-
-describe('MainListFilter Component', () => {
-  it('renders given items', () => {
-    let wrapper = wrap({ items, selectionInfo })
-    expect(wrapper.findPartialId('filterItem').length).toBe(items.length)
-    items.forEach(item => {
-      expect(wrapper.findText(`filterItem-${item.value}`)).toBe(item.label)
-    })
-  })
-
-  it('renders selection info', () => {
-    let wrapper = wrap({ items, selectionInfo })
-    expect(wrapper.findText('selectionText')).toBe('2 of 7 items(s) selected')
-  })
-
-  it('handles reload click', () => {
-    let onReloadButtonClick = sinon.spy()
-    let wrapper = wrap({ items, 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, selectionInfo, onFilterItemClick })
-    wrapper.find(`filterItem-${items[2].value}`).simulate('click')
-    expect(onFilterItemClick.args[0][0].value).toBe(items[2].value)
-  })
-})
-
-
-

+ 0 - 7
src/components/ui/Lists/MainListItem/package.json

@@ -1,7 +0,0 @@
-{
-  "name": "MainListItem",
-  "version": "0.0.0",
-  "private": true,
-  "main":"./MainListItem.tsx"
-}
-

+ 34 - 0
src/components/ui/LoadingButton/LoadingButton.spec.tsx

@@ -0,0 +1,34 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import LoadingButton from '@src/components/ui/LoadingButton'
+import TestUtils from '@tests/TestUtils'
+
+describe('LoadingButton', () => {
+  it('shows the label and rotation animation', () => {
+    render(<LoadingButton>Testing ...</LoadingButton>)
+    expect(TestUtils.select('Button__StyledButton')!.textContent).toBe('Testing ...')
+    const style = window.getComputedStyle(TestUtils.select('LoadingButton__Loading')!)
+    expect(style.animation).toContain('rotate')
+  })
+
+  it('doesn\'t fire click on click', () => {
+    const onClick = jest.fn()
+    render(<LoadingButton onClick={onClick}>Testing ...</LoadingButton>)
+    TestUtils.select('Button__StyledButton')!.click()
+    expect(onClick).not.toHaveBeenCalled()
+  })
+})

+ 0 - 29
src/components/ui/LoadingButton/test.tsx

@@ -1,29 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import TestWrapper from '@src/utils/TestWrapper'
-import LoadingButton from '.'
-
-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 />')
-  })
-})
-
-
-

+ 13 - 17
src/components/ui/SmallLoading/test.tsx → src/components/ui/Logo/Logo.spec.tsx

@@ -1,5 +1,5 @@
 /*
 /*
-Copyright (C) 2017  Cloudbase Solutions SRL
+Copyright (C) 2021  Cloudbase Solutions SRL
 This program is free software: you can redistribute it and/or modify
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
 it under the terms of the GNU Affero General Public License as
 published by the Free Software Foundation, either version 3 of the
 published by the Free Software Foundation, either version 3 of the
@@ -13,23 +13,19 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
 import React from 'react'
 import React from 'react'
-import { shallow } from 'enzyme'
+import { render } from '@testing-library/react'
+import Logo from '@src/components/ui/Logo'
+import TestUtils from '@tests/TestUtils'
 
 
-import TW from '@src/utils/TestWrapper'
-import Component, { TEST_ID } from '.'
-import type { Props } from '.'
-
-const defaultProps: Props = {
-  loadingProgress: 33,
-}
-const wrap = (props: Props) => new TW(shallow(<Component {...props} />), TEST_ID)
-
-describe('SmallLoading Component', () => {
+jest.mock('react-router-dom', () => ({ Link: 'a' }))
+describe('Logo', () => {
   it('renders', () => {
   it('renders', () => {
-    let wrapper = wrap(defaultProps)
-    expect(wrapper.findText('progressText')).toBe('33%')
+    render(<Logo />)
+    expect(TestUtils.select('Logo__Coriolis')).toBeTruthy()
   })
   })
-})
-
-
 
 
+  it('accepts custom \'to\' property', () => {
+    render(<Logo to="#testing" />)
+    expect(TestUtils.select('Logo__LinkStyled')?.getAttribute('to')).toBe('#testing')
+  })
+})

+ 33 - 0
src/components/ui/Modal/Modal.spec.tsx

@@ -0,0 +1,33 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import Modal from '@src/components/ui/Modal'
+import TestUtils from '@tests/TestUtils'
+
+describe('Modal', () => {
+  it('renders content', () => {
+    render(<Modal isOpen title="Test Title"><div className="content">Test Content</div></Modal>)
+    expect(TestUtils.select('ReactModal__Content')!.querySelector('.content')!.textContent).toBe('Test Content')
+    expect(TestUtils.select('Modal__Title')!.textContent).toBe('Test Title')
+  })
+
+  it('requests close on overlay click', () => {
+    const onRequestClose = jest.fn()
+    render(<Modal isOpen onRequestClose={onRequestClose}><div>Test Content</div></Modal>)
+    TestUtils.select('ReactModal__Overlay')!.click()
+    expect(onRequestClose).toHaveBeenCalled()
+  })
+})

+ 0 - 38
src/components/ui/Modal/test.tsx

@@ -1,38 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import TestWrapper from '@src/utils/TestWrapper'
-import Modal from '.'
-
-const wrap = props => new TestWrapper(shallow(<Modal {...props} />), 'modal')
-
-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: 'the_title' })
-    expect(wrapper.findText('child', true)).toBe('Modal')
-    expect(wrapper.find('child').prop('onResizeUpdate')).toBeTruthy()
-  })
-})
-
-
-

+ 90 - 0
src/components/ui/Pagination/Pagination.spec.tsx

@@ -0,0 +1,90 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import Pagination from '@src/components/ui/Pagination'
+import TestUtils from '@tests/TestUtils'
+
+const PaginationWithDefaultProps = (props: Partial<Pagination['props']>) => (
+  <Pagination
+    currentPage={2}
+    totalPages={10}
+    onPreviousClick={props.onPreviousClick || (() => { })}
+    onNextClick={props.onNextClick || (() => { })}
+    nextDisabled={props.nextDisabled || false}
+    previousDisabled={props.previousDisabled || false}
+    loading={props.loading || false}
+  />
+)
+
+describe('Pagination', () => {
+  it('renders', () => {
+    render(<PaginationWithDefaultProps />)
+    expect(TestUtils.select('Pagination__PageNumber')?.textContent).toBe('2 of 10')
+  })
+
+  it('handles previous and next click', () => {
+    const onPreviousClick = jest.fn()
+    const onNextClick = jest.fn()
+    render(
+      <PaginationWithDefaultProps
+        onPreviousClick={onPreviousClick}
+        onNextClick={onNextClick}
+      />,
+    )
+    TestUtils.select('Pagination__PagePrevious')!.click()
+    expect(onPreviousClick).toHaveBeenCalled()
+    TestUtils.select('Pagination__PageNext')!.click()
+    expect(onNextClick).toHaveBeenCalled()
+  })
+
+  it('handles disabled states', () => {
+    let onPreviousClick = jest.fn()
+    let onNextClick = jest.fn()
+    const { rerender } = render(
+      <PaginationWithDefaultProps
+        onPreviousClick={onPreviousClick}
+        previousDisabled
+        onNextClick={onNextClick}
+      />,
+    )
+    TestUtils.select('Pagination__PagePrevious')!.click()
+    expect(onPreviousClick).not.toHaveBeenCalled()
+    TestUtils.select('Pagination__PageNext')!.click()
+    expect(onNextClick).toHaveBeenCalled()
+
+    onPreviousClick = jest.fn()
+    onNextClick = jest.fn()
+    rerender(
+      <PaginationWithDefaultProps
+        onPreviousClick={onPreviousClick}
+        onNextClick={onNextClick}
+        nextDisabled
+      />,
+    )
+    TestUtils.select('Pagination__PagePrevious')!.click()
+    expect(onPreviousClick).toHaveBeenCalled()
+    TestUtils.select('Pagination__PageNext')!.click()
+    expect(onNextClick).not.toHaveBeenCalled()
+  })
+
+  it('shows loading', () => {
+    const { rerender } = render(<PaginationWithDefaultProps />)
+    expect(TestUtils.select('HorizontalLoading__Wrapper')).toBeFalsy()
+
+    rerender(<PaginationWithDefaultProps loading />)
+    expect(TestUtils.select('HorizontalLoading__Wrapper')).toBeTruthy()
+  })
+})

+ 2 - 2
src/components/ui/Pagination/Pagination.tsx

@@ -65,12 +65,12 @@ const PageNumber = styled.div<any>`
 type Props = {
 type Props = {
   className?: string,
   className?: string,
   style?: any,
   style?: any,
-  previousDisabled: boolean,
+  previousDisabled?: boolean,
   onPreviousClick: () => void,
   onPreviousClick: () => void,
   currentPage: number,
   currentPage: number,
   totalPages: number,
   totalPages: number,
   loading?: boolean,
   loading?: boolean,
-  nextDisabled: boolean,
+  nextDisabled?: boolean,
   onNextClick: () => void,
   onNextClick: () => void,
 }
 }
 
 

+ 81 - 0
src/components/ui/Panel/Panel.spec.tsx

@@ -0,0 +1,81 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import Panel from '@src/components/ui/Panel'
+import TestUtils from '@tests/TestUtils'
+import { ThemePalette } from '@src/components/Theme'
+
+const NAVIGATION_ITEMS: Panel['props']['navigationItems'] = [
+  { label: 'Item 1', value: 'item1' },
+  { label: 'Item 2', value: 'item2', title: 'Item 2 title' },
+  { label: 'Item 3', value: 'item3', disabled: true },
+  { label: 'Item 4', value: 'item4', loading: true },
+]
+
+const PanelWithDefaultProps = (props: Partial<Panel['props']>) => (
+  <Panel
+    navigationItems={NAVIGATION_ITEMS}
+    content={<div>Content</div>}
+    selectedValue="item2"
+    onChange={props.onChange || (() => { })}
+    reloadLabel="Reload"
+    onReloadClick={props.onReloadClick || (() => { })}
+  />
+)
+
+describe('Panel', () => {
+  it('renders the items', () => {
+    render(<PanelWithDefaultProps />)
+    const items = TestUtils.selectAll('Panel__NavigationItem')
+    expect(items.length).toBe(NAVIGATION_ITEMS.length)
+    expect(items[0].textContent).toBe(NAVIGATION_ITEMS[0].label)
+    expect(items[1].getAttribute('title')).toBe(NAVIGATION_ITEMS[1].title)
+    expect(items[2].hasAttribute('disabled')).toBe(true)
+    expect(TestUtils.select('Panel__Loading', items[3])).toBeTruthy()
+    expect(TestUtils.select('Panel__Content')!.textContent).toBe('Content')
+
+    const selectedStyle = window.getComputedStyle(items[1])
+    const notSelectedStyle = window.getComputedStyle(items[0])
+    expect(TestUtils.rgbToHex(selectedStyle.color)).toBe(ThemePalette.primary)
+    expect(notSelectedStyle.color).toBe('black')
+
+    const disabledStyle = window.getComputedStyle(items[2])
+    const notDisabledStyle = window.getComputedStyle(items[0])
+    expect(TestUtils.rgbToHex(disabledStyle.color)).toBe(ThemePalette.grayscale[3])
+    expect(notDisabledStyle.color).toBe('black')
+  })
+
+  it('fires change', () => {
+    const items = () => TestUtils.selectAll('Panel__NavigationItem')
+
+    let onChange = jest.fn()
+    const { rerender } = render(<PanelWithDefaultProps onChange={onChange} />)
+    items()[0].click()
+
+    onChange = jest.fn()
+    rerender(<PanelWithDefaultProps onChange={onChange} />)
+    items()[1].click() // currently selected
+    items()[2].click() // disabled
+    expect(onChange).not.toHaveBeenCalled()
+  })
+
+  it('fires reload', () => {
+    const onReloadClick = jest.fn()
+    render(<PanelWithDefaultProps onReloadClick={onReloadClick} />)
+    TestUtils.select('Panel__ReloadButton')!.click()
+    expect(onReloadClick).toHaveBeenCalled()
+  })
+})

+ 2 - 4
src/components/ui/Panel/Panel.tsx

@@ -72,7 +72,7 @@ const Loading = styled.span`
   ${ThemeProps.animations.rotation}
   ${ThemeProps.animations.rotation}
 `
 `
 
 
-export type NavigationItem = {
+type NavigationItem = {
   label: string,
   label: string,
   value: string,
   value: string,
   disabled?: boolean,
   disabled?: boolean,
@@ -80,7 +80,7 @@ export type NavigationItem = {
   loading?: boolean,
   loading?: boolean,
 }
 }
 
 
-export type Props = {
+type Props = {
   navigationItems: NavigationItem[],
   navigationItems: NavigationItem[],
   content: React.ReactNode,
   content: React.ReactNode,
   selectedValue: string | null,
   selectedValue: string | null,
@@ -90,8 +90,6 @@ export type Props = {
   onReloadClick: () => void,
   onReloadClick: () => void,
 }
 }
 
 
-export const TEST_ID = 'panel'
-
 @observer
 @observer
 class Panel extends React.Component<Props> {
 class Panel extends React.Component<Props> {
   handleItemClick(item: NavigationItem) {
   handleItemClick(item: NavigationItem) {

+ 0 - 91
src/components/ui/Panel/test.tsx

@@ -1,91 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import sinon from 'sinon'
-
-import TW from '@src/utils/TestWrapper'
-import Panel, { TEST_ID } from '.'
-import type { Props, NavigationItem } from '.'
-
-const navigationItems: NavigationItem[] = [
-  { label: 'Navigation1', value: 'navigation1' },
-  { label: 'Navigation2', value: 'navigation2' },
-]
-
-const content = 'Content'
-
-const wrap = (props: Props) => new TW(shallow(
-  <Panel {...props} />
-), TEST_ID)
-
-describe('Panel Component', () => {
-  it('renders navigation items', () => {
-    let wrapper = wrap({
-      navigationItems,
-      content,
-      reloadLabel: '',
-      onChange: () => { },
-      selectedValue: 'navigation2',
-      onReloadClick: () => { },
-    })
-    navigationItems.forEach(i => {
-      expect(wrapper.findText(`navItem-${i.value}`)).toBe(i.label)
-    })
-  })
-
-  it('selects the selected value', () => {
-    let wrapper = wrap({
-      navigationItems,
-      content,
-      reloadLabel: '',
-      onChange: () => { },
-      selectedValue: 'navigation2',
-      onReloadClick: () => { },
-
-    })
-    expect(wrapper.find('navItem-navigation1').prop('selected')).toBeFalsy()
-    expect(wrapper.find('navItem-navigation2').prop('selected')).toBe(true)
-  })
-
-  it('dispatches onChange', () => {
-    let onChange = sinon.spy()
-    let wrapper = wrap({
-      navigationItems,
-      content,
-      onChange,
-      reloadLabel: '',
-      selectedValue: 'navigation2',
-      onReloadClick: () => { },
-    })
-    wrapper.find('navItem-navigation1').simulate('click')
-    expect(onChange.called).toBe(true)
-  })
-
-  it('renders content', () => {
-    let wrapper = wrap({
-      navigationItems,
-      content,
-      reloadLabel: '',
-      onChange: () => { },
-      selectedValue: 'navigation2',
-      onReloadClick: () => { },
-    })
-    expect(wrapper.findText('content')).toBe(content)
-  })
-})
-
-
-

+ 12 - 15
src/components/ui/PasswordValue/test.tsx → src/components/ui/PasswordValue/PasswordValue.spec.tsx

@@ -1,5 +1,5 @@
 /*
 /*
-Copyright (C) 2017  Cloudbase Solutions SRL
+Copyright (C) 2021  Cloudbase Solutions SRL
 This program is free software: you can redistribute it and/or modify
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
 it under the terms of the GNU Affero General Public License as
 published by the Free Software Foundation, either version 3 of the
 published by the Free Software Foundation, either version 3 of the
@@ -13,21 +13,18 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
 import React from 'react'
 import React from 'react'
-import { shallow } from 'enzyme'
-import TestWrapper from '@src/utils/TestWrapper'
-import PasswordValue from '.'
+import { render } from '@testing-library/react'
+import PasswordValue from '@src/components/ui/PasswordValue'
+import TestUtils from '@tests/TestUtils'
 
 
-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('•••••••••')
+describe('PasswordValue', () => {
+  it('hides the password', () => {
+    render(<PasswordValue value="the_secret" />)
+    expect(TestUtils.select('PasswordValue__Value')?.textContent).toBe('•••••••••')
   })
   })
-
-  it('reveals password on click', () => {
-    const wrapper = wrap()
-    wrapper.simulate('click')
-    expect(wrapper.findText('value')).toBe('the_value')
+  it('reveals the password on click', () => {
+    render(<PasswordValue value="the_secret" />)
+    TestUtils.select('PasswordValue__Value')?.click()
+    expect(TestUtils.select('PasswordValue__Value')?.textContent).toBe('the_secret')
   })
   })
 })
 })

+ 12 - 18
src/components/ui/SearchInput/test.tsx → src/components/ui/ProgressBar/ProgressBar.spec.tsx

@@ -1,5 +1,5 @@
 /*
 /*
-Copyright (C) 2017  Cloudbase Solutions SRL
+Copyright (C) 2021  Cloudbase Solutions SRL
 This program is free software: you can redistribute it and/or modify
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
 it under the terms of the GNU Affero General Public License as
 published by the Free Software Foundation, either version 3 of the
 published by the Free Software Foundation, either version 3 of the
@@ -13,25 +13,19 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
 import React from 'react'
 import React from 'react'
-import { shallow } from 'enzyme'
-import TW from '@src/utils/TestWrapper'
-import SearchInput from '../SearchInput'
+import { render } from '@testing-library/react'
+import ProgressBar from '@src/components/ui/ProgressBar'
+import TestUtils from '@tests/TestUtils'
 
 
-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('button').simulate('click')
-    expect(wrapper.prop('open')).toBe(true)
+describe('ProgressBar', () => {
+  it('renders the progress indicator with the correct width', () => {
+    render(<ProgressBar progress={33} />)
+    const style = window.getComputedStyle(TestUtils.select('ProgressBar__Progress-')!)
+    expect(style.width).toBe('33%')
   })
   })
 
 
-  it('has loading state', () => {
-    let wrapper = wrap({ loading: true })
-    expect(wrapper.find('loading').length).toBe(1)
+  it('shows progress label', () => {
+    render(<ProgressBar progress={33} useLabel />)
+    expect(TestUtils.select('ProgressBar__ProgressLabel')?.textContent).toBe('33 %')
   })
   })
 })
 })
-
-
-

+ 0 - 30
src/components/ui/ProgressBar/test.tsx

@@ -1,30 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import TestWrapper from '@src/utils/TestWrapper'
-import ProgressBar from '.'
-
-const wrap = props => new TestWrapper(shallow(<ProgressBar {...props} />), 'progressBar')
-
-describe('ProgressBar Component', () => {
-  it('has width according to progress number', () => {
-    const wrapper = wrap({ progress: 61 })
-    expect(wrapper.find('progress').prop('width')).toBe(61)
-  })
-})
-
-
-

+ 113 - 0
src/components/ui/PropertiesTable/PropertiesTable.spec.tsx

@@ -0,0 +1,113 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import PropertiesTable from '@src/components/ui/PropertiesTable'
+import { Field } from '@src/@types/Field'
+import TestUtils from '@tests/TestUtils'
+import userEvent from '@testing-library/user-event'
+
+const PROPERTIES: Field[] = [
+  {
+    name: 'name',
+    type: 'string',
+  },
+  {
+    name: 'name2',
+    type: 'string',
+    required: true,
+    enum: ['a', 'b', 'c'],
+  },
+  {
+    name: 'name3',
+    type: 'boolean',
+  },
+  {
+    name: 'name4',
+    type: 'boolean',
+    nullableBoolean: true,
+  },
+]
+
+describe('PropertiesTable', () => {
+  it('renders all properties', () => {
+    render(
+      <PropertiesTable
+        properties={PROPERTIES}
+        onChange={() => { }}
+        valueCallback={field => (field.type === 'string' ? `${field.name}-value` : null)}
+      />,
+    )
+    expect(TestUtils.selectInput('TextInput__Input')?.value).toBe('name-value')
+
+    TestUtils.select('DropdownButton__Wrapper')?.click()
+    const listItems = TestUtils.selectAll('Dropdown__ListItem-')
+    expect(listItems.length).toBe(PROPERTIES[1].enum!.length + 1)
+    expect(listItems[2].textContent).toBe('B')
+    expect(TestUtils.select('Dropdown__Required')).toBeTruthy()
+
+    const switches = TestUtils.selectAll('Switch__Wrapper')
+    expect(switches.length).toBe(2)
+    expect(switches[0].textContent).toBe('No')
+    expect(switches[1].textContent).toBe('Not Set')
+  })
+
+  it('dispatches text input change', () => {
+    const onChange = jest.fn()
+    render(
+      <PropertiesTable
+        properties={PROPERTIES}
+        onChange={onChange}
+        valueCallback={() => {}}
+      />,
+    )
+    userEvent.clear(TestUtils.selectInput('TextInput__Input')!)
+    userEvent.type(TestUtils.selectInput('TextInput__Input')!, 'new-value')
+    expect(onChange).toHaveBeenCalledWith(PROPERTIES[0], 'new-value')
+  })
+
+  it('dispatches dropdown change', () => {
+    const onChange = jest.fn()
+    render(
+      <PropertiesTable
+        properties={PROPERTIES}
+        onChange={onChange}
+        valueCallback={() => {}}
+      />,
+    )
+    TestUtils.select('DropdownButton__Wrapper')!.click()
+    TestUtils.selectAll('Dropdown__ListItem-')[2]!.click()
+    expect(onChange).toHaveBeenCalledWith(PROPERTIES[1], 'b')
+  })
+
+  it('dispatches switch change', () => {
+    const onChange = jest.fn()
+    render(
+      <PropertiesTable
+        properties={PROPERTIES}
+        onChange={onChange}
+        valueCallback={() => true}
+      />,
+    )
+
+    const [nonNullableSwitch, nullableSwitch] = TestUtils.selectAll('Switch__InputWrapper')
+
+    nonNullableSwitch.click()
+    expect(onChange).toHaveBeenCalledWith(PROPERTIES[2], false)
+
+    nullableSwitch.click()
+    expect(onChange).toHaveBeenCalledWith(PROPERTIES[3], null)
+  })
+})

+ 0 - 66
src/components/ui/PropertiesTable/test.tsx

@@ -1,66 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import TW from '@src/utils/TestWrapper'
-import PropertiesTable from '.'
-
-const wrap = props => new TW(shallow(<PropertiesTable onChange={() => { }} {...props} />), 'propertiesTable')
-
-let properties = [
-  { type: 'boolean', name: 'prop_1', label: 'Nullabel Boolean', value: true, nullableBoolean: true },
-  { type: 'boolean', name: 'prop_2', label: '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
-}
-
-describe('PropertiesTable Component', () => {
-  it('renders all properties', () => {
-    const wrapper = wrap({ properties, valueCallback })
-    expect(wrapper.findPartialId('row-').length).toBe(properties.length)
-    expect(wrapper.findPartialId(`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)
-  })
-
-  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')
-  })
-})
-
-
-

+ 44 - 0
src/components/ui/RadioInput/RadioInput.spec.tsx

@@ -0,0 +1,44 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import RadioInput from '@src/components/ui/RadioInput'
+import TestUtils from '@tests/TestUtils'
+
+describe('RadioInput', () => {
+  it('renders', () => {
+    render(
+      <RadioInput
+        label="Label"
+        checked
+        onChange={() => {}}
+      />,
+    )
+    expect(TestUtils.select('RadioInput__Text')?.textContent).toBe('Label')
+  })
+
+  it('renders disabled loading animation', () => {
+    render(
+      <RadioInput
+        label="Label"
+        checked
+        onChange={() => {}}
+        disabledLoading
+      />,
+    )
+    const style = window.getComputedStyle(TestUtils.select('RadioInput__Wrapper')!)
+    expect(style.animation).toContain('opacityToggle')
+  })
+})

+ 0 - 30
src/components/ui/RadioInput/test.tsx

@@ -1,30 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import TestWrapper from '@src/utils/TestWrapper'
-import RadioInput from '.'
-
-const wrap = props => new TestWrapper(shallow(<RadioInput {...props} />), 'radioInput')
-
-describe('RadioInput Component', () => {
-  it('renders the given label', () => {
-    const wrapper = wrap({ label: 'the_value', onChange: () => { } })
-    expect(wrapper.findText('label')).toBe('the_value')
-  })
-})
-
-
-

+ 34 - 0
src/components/ui/ReloadButton/ReloadButton.spec.tsx

@@ -0,0 +1,34 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import ReloadButton from '.'
+
+describe('ReloadButton', () => {
+  it('fires click', () => {
+    const onClick = jest.fn()
+    render(<ReloadButton onClick={onClick} />)
+    TestUtils.select('ReloadButton__Wrapper')!.click()
+    expect(onClick).toHaveBeenCalled()
+  })
+
+  it('shows click animation', () => {
+    render(<ReloadButton onClick={() => { }} />)
+    expect(TestUtils.select('ReloadButton__Wrapper')!.classList).not.toContain('reload-animation')
+    TestUtils.select('ReloadButton__Wrapper')!.click()
+    expect(TestUtils.select('ReloadButton__Wrapper')!.classList).toContain('reload-animation')
+  })
+})

+ 0 - 32
src/components/ui/ReloadButton/test.tsx

@@ -1,32 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import sinon from 'sinon'
-import ReloadButton from '.'
-
-const wrap = props => shallow(<ReloadButton {...props} />)
-
-describe('ReloadButton Component', () => {
-  it('handles click', () => {
-    let onClick = sinon.spy()
-    let wrapper = wrap({ onClick })
-    wrapper.simulate('click')
-    expect(onClick.calledOnce).toBe(true)
-  })
-})
-
-
-

+ 8 - 11
src/components/ui/StatusComponents/StatusIcon/test.tsx → src/components/ui/SearchButton/SearchButton.spec.tsx

@@ -1,5 +1,5 @@
 /*
 /*
-Copyright (C) 2017  Cloudbase Solutions SRL
+Copyright (C) 2021  Cloudbase Solutions SRL
 This program is free software: you can redistribute it and/or modify
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
 it under the terms of the GNU Affero General Public License as
 published by the Free Software Foundation, either version 3 of the
 published by the Free Software Foundation, either version 3 of the
@@ -13,16 +13,13 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
 import React from 'react'
 import React from 'react'
-import { shallow } from 'enzyme'
-import StatusIcon from '.'
+import { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import SearchButton from '.'
 
 
-const wrap = props => shallow(<StatusIcon {...props} />)
-
-describe('StatusIcon Component', () => {
-  it('renders with props', () => {
-    expect(wrap({ status: 'success' }).prop('status')).toBe('success')
+describe('SearchButton', () => {
+  it('renders', () => {
+    render(<SearchButton />)
+    expect(TestUtils.select('SearchButton__Icon')).toBeTruthy()
   })
   })
 })
 })
-
-
-

+ 0 - 47
src/components/ui/SearchButton/test.tsx

@@ -1,47 +0,0 @@
-/*
-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 { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TestWrapper from '@src/utils/TestWrapper'
-import SearchButton from '.'
-
-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
-      const iconSvgId = /data--id="(.*?)"/g.exec(iconSvg)
-      return iconSvgId ? iconSvgId[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)
-  })
-})
-
-
-

+ 57 - 0
src/components/ui/SearchInput/SearchInput.spec.tsx

@@ -0,0 +1,57 @@
+/*
+Copyright (C) 2021  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 { render } from '@testing-library/react'
+import TestUtils from '@tests/TestUtils'
+import userEvent from '@testing-library/user-event'
+import { ThemeProps } from '@src/components/Theme'
+import SearchInput from '.'
+
+describe('SearchInput', () => {
+  it('renders the value', () => {
+    render(<SearchInput value="test value" />)
+    expect(TestUtils.selectInput('TextInput__Input')!.value).toBe('test value')
+  })
+
+  it('fires change on input change', () => {
+    const onChange = jest.fn()
+    render(<SearchInput onChange={onChange} />)
+    userEvent.paste(TestUtils.selectInput('TextInput__Input')!, 'test value')
+    expect(onChange).toHaveBeenCalledWith('test value')
+  })
+
+  it('opens on button click', async () => {
+    render(<SearchInput />)
+    const style = () => window.getComputedStyle(TestUtils.selectInput('TextInput__Input')!)
+    expect(style().width).toBe('50px')
+    TestUtils.select('SearchButton__Wrapper')!.click()
+    // wait 500 ms for animation
+    await new Promise(resolve => { setTimeout(resolve, 500) })
+    expect(style().width).toBe(`${ThemeProps.inputSizes.regular.width}px`)
+  })
+
+  it('renders open when it has alwaysOpen prop', () => {
+    render(<SearchInput alwaysOpen />)
+    const style = window.getComputedStyle(TestUtils.selectInput('TextInput__Input')!)
+    expect(style.width).toBe(`${ThemeProps.inputSizes.regular.width}px`)
+  })
+
+  it('renders loading state', () => {
+    const { rerender } = render(<SearchInput loading />)
+    expect(TestUtils.select('StatusIcon__Wrapper')).toBeTruthy()
+    rerender(<SearchInput />)
+    expect(TestUtils.select('StatusIcon__Wrapper')).toBeFalsy()
+  })
+})

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio