Bläddra i källkod

Add unit tests

Sergiu Miclea 4 år sedan
förälder
incheckning
4a7432971b
100 ändrade filer med 2510 tillägg och 1588 borttagningar
  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 { 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'
 
 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
-  // 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
   // modulePathIgnorePatterns: [],
@@ -126,7 +129,9 @@ export default {
   // runner: "jest-runner",
 
   // 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
   // setupFilesAfterEnv: [],
@@ -172,7 +177,10 @@ export default {
   // timers: "real",
 
   // 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
   // transformIgnorePatterns: [

+ 10 - 6
package.json

@@ -14,12 +14,15 @@
     "tsc": "npx tsc --skipLibCheck",
     "eslint": "npx eslint \"src/**\" \"server/**\"",
     "test": "jest",
-    "test-release": "node ./server/testRelease",
+    "test-release": "node ./tests/testRelease",
+    "test-coverage": "node ./tests/testCoverage",
     "storybook": "start-storybook"
   },
   "devDependencies": {
     "@storybook/react": "^6.4.13",
+    "@testing-library/dom": "^8.11.1",
     "@testing-library/react": "^12.1.2",
+    "@testing-library/user-event": "^13.5.0",
     "@types/connect": "^3.4.33",
     "@types/express": "^4.17.6",
     "@types/file-saver": "^2.0.1",
@@ -30,11 +33,12 @@
     "@types/react-dom": "^16.9.8",
     "@types/react-modal": "^3.10.5",
     "@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/styled-components": "^5.1.0",
     "@typescript-eslint/eslint-plugin": "^5.6.0",
     "@typescript-eslint/parser": "^5.6.0",
+    "cross-spawn": "^7.0.3",
     "cypress": "^3.2.0",
     "eslint": "^8.2.0",
     "eslint-config-airbnb": "19.0.0",
@@ -45,8 +49,7 @@
     "eslint-plugin-react": "^7.27.0",
     "eslint-plugin-react-hooks": "^4.3.0",
     "jest": "^27.3.1",
-    "nodemon": "^2.0.4",
-    "ts-node": "^10.4.0"
+    "nodemon": "^2.0.4"
   },
   "dependencies": {
     "@babel/core": "^7.7.2",
@@ -84,7 +87,7 @@
     "path": "^0.12.7",
     "react": "^16.13.1",
     "react-collapse": "^5.0.1",
-    "react-datetime": "^2.10.3",
+    "react-datetime": "^3.1.1",
     "react-dom": "^16.13.1",
     "react-hot-loader": "^4.12.17",
     "react-modal": "^3.11.2",
@@ -98,6 +101,7 @@
     "rimraf": "^2.6.2",
     "styled-components": "^4.4.1",
     "tai-password-strength": "^1.1.2",
+    "ts-node": "10.4.0",
     "typescript": "^4.5.3",
     "url-loader": "^4.1.0",
     "webpack": "^4.41.2",
@@ -125,6 +129,6 @@
     "ua-parser-js": "^0.7.24",
     "underscore": "^1.12.1",
     "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
         path={options.path}
+        // @ts-ignore
         exact={options.exact}
         render={() => (
           <MessagePage
@@ -142,6 +143,7 @@ class App extends React.Component<{}, State> {
           showAuthAnimation: true,
         })
       }
+      // @ts-ignore
       return <Route path={path} component={component} exact={exact} />
     }
 
@@ -173,6 +175,7 @@ class App extends React.Component<{}, State> {
         })
       }
       if (userStore.loggedUser?.isAdmin) {
+        // @ts-ignore
         return <Route path={actualPath} exact={exact} component={component} />
       }
       return null
@@ -184,9 +187,14 @@ class App extends React.Component<{}, State> {
         <Router>
           <Switch>
             {configLoader.isFirstLaunch ? (
+            // @ts-ignore
               <Route path="/" component={SetupPage} exact />
+            // @ts-ignore
             ) : renderRoute('/', DashboardPage, true)}
-            <Route path="/login" component={LoginPage} />
+            {
+              // @ts-ignore
+              <Route path="/login" component={LoginPage} />
+            }
             {renderRoute('/dashboard', DashboardPage)}
             {renderRoute('/replicas', ReplicasPage, true)}
             {renderRoute('/replicas/:id', ReplicaDetailsPage, true)}
@@ -208,7 +216,10 @@ class App extends React.Component<{}, State> {
             {renderOptionalRoute('projects', ProjectDetailsPage, '/projects/:id')}
             {renderOptionalRoute('logging', LogsPage)}
             {renderRoute('/streamlog', LogStreamPage)}
-            <Route component={MessagePage} />
+            {
+              // @ts-ignore
+              <Route component={MessagePage} />
+            }
           </Switch>
         </Router>
         <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 = {
   notificationItems: NotificationItemData[],
-  style: any,
-  loading: boolean,
-  large: boolean,
-  onNewClick: () => void,
+  style?: React.CSSProperties | null,
+  loading?: boolean,
+  large?: boolean,
+  onNewClick?: () => void,
 }
 @observer
 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 styled from 'styled-components'
 
-import { ThemeProps } from '@src/components/Theme'
+import { ThemePalette, ThemeProps } from '@src/components/Theme'
 import BarChartNiceScale from './BarChartNiceScale'
 
 const Wrapper = styled.div<any>`
@@ -94,9 +94,7 @@ type DataItem = {
 }
 type Props = {
   style?: any,
-  // eslint-disable-next-line react/no-unused-prop-types
   data: DataItem[],
-  // eslint-disable-next-line react/no-unused-prop-types
   yNumTicks: number,
   colors?: string[],
   onBarMouseEnter?: (position: { x: number, y: number }, item: DataItem) => void,
@@ -192,7 +190,7 @@ class DashboardBarChart extends React.Component<Props> {
                   <StackedBar
                     // eslint-disable-next-line react/no-array-index-key
                     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}
                     onMouseEnter={(evt: MouseEvent) => {
                       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 },
   tooltipData: TooltipData | null,
 }
-const COLORS = ['#F91661', '#0044CB']
+const COLORS = [ThemePalette.alert, ThemePalette.primary]
 
 @observer
 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 {
   UpdateData, TransferItemDetails, MigrationItemDetails,
 } from '@src/@types/MainItem'
-import type { NavigationItem } from '@src/components/ui/Panel'
 import {
   Endpoint, EndpointUtils, StorageBackend, StorageMap,
 } from '@src/@types/Endpoint'
@@ -787,7 +786,7 @@ class TransferItemModal extends React.Component<Props, State> {
   }
 
   render() {
-    const navigationItems: NavigationItem[] = [
+    const navigationItems: Panel['props']['navigationItems'] = [
       {
         value: '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,
 }
 @observer
-class MainListItem extends React.Component<Props> {
+class TransferListItem extends React.Component<Props> {
   getStatus() {
     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 { storiesOf } from '@storybook/react'
-import MainListItem from '.'
+import TransferListItem from './TransferListItem'
 
 const item: any = {
   origin_endpoint_id: 'openstack',
@@ -32,7 +32,7 @@ const endpointType = (id: any) => id
 
 storiesOf('MainListItem', module)
   .add('completed', () => (
-    <MainListItem
+    <TransferListItem
       item={item}
       endpointType={endpointType}
       selected={false}
@@ -44,7 +44,7 @@ storiesOf('MainListItem', module)
     />
   ))
   .add('running', () => (
-    <MainListItem
+    <TransferListItem
       item={item2}
       endpointType={endpointType}
       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 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 DropdownFilterGroup from '@src/components/ui/Dropdowns/DropdownFilterGroup'
 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 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 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 Navigation from '@src/components/modules/NavigationModule/Navigation'
 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 AlertModal from '@src/components/ui/AlertModal'
 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 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 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 Navigation from '@src/components/modules/NavigationModule/Navigation'
 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 MainListItem from '@src/components/ui/Lists/MainListItem'
 
 import projectStore from '@src/stores/ProjectStore'
 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 { MigrationItem } from '@src/@types/MainItem'
 import userStore from '@src/stores/UserStore'
+import TransferListItem from '@src/components/modules/TransferModule/TransferListItem'
 import migrationLargeImage from './images/migration-large.svg'
 import migrationItemImage from './images/migration.svg'
 
@@ -254,7 +254,7 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
               }}
               dropdownActions={BulkActions}
               renderItemComponent={options => (
-                <MainListItem
+                <TransferListItem
                   {...options}
                   image={migrationItemImage}
                   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 Navigation from '@src/components/modules/NavigationModule/Navigation'
 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'
 

+ 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 Navigation from '@src/components/modules/NavigationModule/Navigation'
 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 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 Navigation from '@src/components/modules/NavigationModule/Navigation'
 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 MainListItem from '@src/components/ui/Lists/MainListItem'
 import Modal from '@src/components/ui/Modal'
 import ReplicaExecutionOptions from '@src/components/modules/TransferModule/ReplicaExecutionOptions'
 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 { ReplicaItem } from '@src/@types/MainItem'
 import userStore from '@src/stores/UserStore'
+import TransferListItem from '@src/components/modules/TransferModule/TransferListItem'
 import replicaLargeImage from './images/replica-large.svg'
 import replicaItemImage from './images/replica.svg'
 
@@ -352,7 +352,7 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
               onSelectedItemsChange={selectedReplicas => { this.setState({ selectedReplicas }) }}
               onPaginatedItemsChange={paginatedReplicas => { this.handlePaginatedItemsChange(paginatedReplicas) }}
               renderItemComponent={options => (
-                <MainListItem
+                <TransferListItem
                   {...options}
                   image={replicaItemImage}
                   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 Navigation from '@src/components/modules/NavigationModule/Navigation'
 import FilterList from '@src/components/ui/Lists/FilterList'
-import PageHeader from '@src/components/ui/PageHeader'
 import UserListItem from '@src/components/modules/UserModule/UserListItem'
 
 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 userStore from '@src/stores/UserStore'
 import configLoader from '@src/utils/Config'
+import PageHeader from '@src/components/smart/PageHeader'
 
 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
 it under the terms of the GNU Affero General Public License as
 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 { 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() {
     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 === Number(this.state.searchText)
+      : item.value === Number(this.state.searchText))
         || 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
     }
     const fileName = files[0].name
-    const content = await FileUtils.readTextFromFirstFile(files)
     this.setState({ fileName })
+    const content = await FileUtils.readTextFromFirstFile(files)
     if (this.props.onUpload) {
       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
 it under the terms of the GNU Affero General Public License as
 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 { 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
-      this.setState({
+      return {
         selectedItems,
         selectAllSelected,
         filterStatus: item.value,
         items,
         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
 it under the terms of the GNU Affero General Public License as
 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 { 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', () => {
-    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 = {
   className?: string,
   style?: any,
-  previousDisabled: boolean,
+  previousDisabled?: boolean,
   onPreviousClick: () => void,
   currentPage: number,
   totalPages: number,
   loading?: boolean,
-  nextDisabled: boolean,
+  nextDisabled?: boolean,
   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}
 `
 
-export type NavigationItem = {
+type NavigationItem = {
   label: string,
   value: string,
   disabled?: boolean,
@@ -80,7 +80,7 @@ export type NavigationItem = {
   loading?: boolean,
 }
 
-export type Props = {
+type Props = {
   navigationItems: NavigationItem[],
   content: React.ReactNode,
   selectedValue: string | null,
@@ -90,8 +90,6 @@ export type Props = {
   onReloadClick: () => void,
 }
 
-export const TEST_ID = 'panel'
-
 @observer
 class Panel extends React.Component<Props> {
   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
 it under the terms of the GNU Affero General Public License as
 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 { 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
 it under the terms of the GNU Affero General Public License as
 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 { 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
 it under the terms of the GNU Affero General Public License as
 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 { 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()
+  })
+})

Vissa filer visades inte eftersom för många filer har ändrats