From f0349f13faea2d892befefb339b7236e3fe1d00f Mon Sep 17 00:00:00 2001 From: Sergiu Miclea Date: Fri, 27 Oct 2023 00:23:42 +0300 Subject: [PATCH] Add unit tests and test coverage report The test coverage is automatically generated when running `npm run test`. A summary of the report is displayed in the console, and a more detailed report is generated in the `coverage` folder. --- .gitignore | 4 + jest.config.ts | 32 +- package.json | 4 +- .../DashboardBarChart.spec.tsx | 143 ++++++- .../DashboardContent.spec.tsx | 131 ++++++ .../DashboardExecutions.spec.tsx | 252 ++++++++++++ .../DashboardInfoCount.spec.tsx | 76 ++++ .../DashboardLicence.spec.tsx | 181 +++++++++ .../DashboardLicence/DashboardLicence.tsx | 2 +- .../DashboardPieChart.spec.tsx | 295 ++++++++++++++ .../DashboardPieChart/DashboardPieChart.tsx | 2 +- .../DashboardTopEndpoints.spec.tsx | 227 +++++++++++ .../DashboardTopEndpoints.tsx | 2 +- .../DetailsContentHeader.spec.tsx | 122 ++++++ .../DetailsContentHeader/test.tsx | 82 ---- .../DetailsPageHeader.spec.tsx | 151 +++++++ .../DetailsPageHeader/DetailsPageHeader.tsx | 4 - .../DetailsModule/DetailsPageHeader/test.tsx | 56 --- .../ChooseProvider/ChooseProvider.spec.tsx | 383 ++++++++++++++++++ .../MultipleUploadedEndpoints.spec.tsx | 288 +++++++++++++ .../MultipleUploadedEndpoints.tsx | 2 +- .../EndpointModule/ChooseProvider/test.tsx | 55 --- .../EndpointDetailsContent.spec.tsx | 334 +++++++++++++++ .../EndpointDetailsContent/test.tsx | 120 ------ .../EndpointDuplicateOptions.spec.tsx | 93 +++++ .../EndpointDuplicateOptions/test.tsx | 75 ---- .../EndpointListItem.spec.tsx | 91 +++++ .../EndpointModule/EndpointListItem/test.tsx | 58 --- .../EndpointLogos/EndpointLogos.spec.tsx | 58 +++ .../EndpointLogos/resources/Generic.spec.tsx | 89 ++++ .../EndpointModule/EndpointLogos/test.tsx | 42 -- .../EndpointValidation.spec.tsx | 117 ++++++ .../EndpointValidation/test.tsx | 53 --- .../LicenceModule/LicenceModule.spec.tsx | 167 ++++++++ .../LoginModule/LoginForm/LoginForm.spec.tsx | 93 +++++ .../LoginModule/LoginForm/LoginForm.tsx | 2 +- .../modules/LoginModule/LoginForm/test.tsx | 54 --- .../LoginModule/LoginFormField/test.tsx | 36 -- .../LoginOptions/LoginOptions.spec.tsx | 59 +++ .../LoginModule/LoginOptions/LoginOptions.tsx | 2 +- .../modules/LoginModule/LoginOptions/test.tsx | 57 --- .../MetalHubListHeader.spec.tsx | 39 ++ .../MetalHubListItem.spec.tsx | 65 +++ .../MetalHubModal/MetalHubModal.spec.tsx | 126 ++++++ .../MetalHubModal/MetalHubModal.tsx | 21 +- .../MetalHubServerDetailsContent.spec.tsx | 101 +++++ .../MinionEndpointModal.spec.tsx | 92 +++++ .../MinionEndpointModal.tsx | 2 +- .../MinionPoolConfirmationModal.spec.tsx | 75 ++++ .../MinionPoolDetailsContent.spec.tsx | 102 +++++ .../MinionPoolEvents.spec.tsx | 137 +++++++ .../MinionPoolEvents.tsx | 11 - .../MinionPoolMachines.spec.tsx | 106 +++++ .../MinionPoolMainDetails.spec.tsx | 74 ++++ .../MinionPoolListItem.spec.tsx | 39 ++ .../MinionPoolModalContent.spec.tsx | 113 ++++++ .../DetailsNavigation/test.tsx | 55 --- .../Navigation/Navigation.tsx | 2 +- .../NavigationMini/NavigationMini.spec.tsx | 42 ++ .../NavigationMini/NavigationMini.tsx | 2 - .../ProjectDetailsContent.spec.tsx | 197 +++++++++ .../ProjectDetailsContent.tsx | 20 +- .../ProjectDetailsContent/test.tsx | 83 ---- .../ProjectListItem/ProjectListItem.spec.tsx | 55 +++ .../ProjectModule/ProjectListItem/test.tsx | 81 ---- .../ProjectModule/ProjectMemberModal/test.tsx | 151 ------- .../ProjectModule/ProjectModal/test.tsx | 75 ---- .../SetupPageEmailBody.spec.tsx | 83 ++++ .../SetupPageEmailBody/SetupPageEmailBody.tsx | 33 +- .../SetupPageHelp/SetupPageHelp.spec.tsx} | 19 +- .../SetupPageHelp/SetupPageHelp.tsx | 4 +- .../SetupPageLegal/SetupPageLegal.spec.tsx | 162 ++++++++ .../SetupPageLicence.spec.tsx | 99 +++++ .../SetupPageModuleWrapper.spec.tsx} | 37 +- .../SetupPageWelcome.spec.tsx} | 26 +- .../SetupPageBackButton.spec.tsx | 37 ++ .../SetupPagePasswordStrength.spec.tsx | 50 +++ .../DetailsTemplate/DetailsTemplate.spec.tsx | 38 ++ .../DetailsTemplate/DetailsTemplate.tsx | 2 +- .../EmptyTemplate/EmptyTemplate.spec.tsx | 30 ++ .../MainTemplate/MainTemplate.spec.tsx | 34 ++ .../WizardTemplate/WizardTemplate.spec.tsx | 32 ++ .../DeleteReplicaModal.spec.tsx | 59 +++ .../DeleteReplicaModal/DeleteReplicaModal.tsx | 7 +- .../Executions/Executions.spec.tsx | 230 +++++++++++ .../TransferModule/Executions/Executions.tsx | 8 +- .../TransferModule/Executions/test.tsx | 111 ----- .../MainDetails/MainDetails.spec.tsx | 105 +++++ .../TransferModule/MainDetails/test.tsx | 80 ---- .../MigrationDetailsContent.spec.tsx | 74 ++++ .../MigrationDetailsContent.tsx | 13 +- .../MigrationDetailsContent/test.tsx | 77 ---- .../ReplicaDetailsContent.spec.tsx | 131 ++++++ .../ReplicaDetailsContent.tsx | 18 +- .../ReplicaDetailsContent/test.tsx | 134 ------ .../ReplicaExecutionOptions.spec.tsx | 75 ++++ .../ReplicaExecutionOptions/test.tsx | 70 ---- .../ReplicaMigrationOptions.spec.tsx | 154 +++++++ .../ReplicaMigrationOptions.tsx | 27 +- .../ReplicaMigrationOptions/test.tsx | 50 --- .../TransferModule/Schedule/Schedule.spec.tsx | 245 +++++++++++ .../TransferModule/Schedule/Schedule.tsx | 6 +- .../modules/TransferModule/Schedule/test.tsx | 102 ----- .../ScheduleItem/ScheduleItem.spec.tsx | 99 +++++ .../TransferModule/ScheduleItem/test.tsx | 67 --- .../TransferModule/TaskItem/TaskItem.spec.tsx | 134 ++++++ .../TransferModule/TaskItem/TaskItem.tsx | 3 - .../modules/TransferModule/TaskItem/test.tsx | 48 --- .../TransferModule/Tasks/Tasks.spec.tsx | 123 ++++++ .../modules/TransferModule/Tasks/Tasks.tsx | 19 +- .../modules/TransferModule/Tasks/test.tsx | 109 ----- .../modules/TransferModule/Timeline/test.tsx | 62 --- .../TransferDetailsTable.spec.tsx | 91 +++++ .../TransferDetailsTable.tsx | 1 - .../TransferDetailsTable/test.tsx | 120 ------ .../TransferListItem.spec.tsx | 42 ++ .../TransferModule/TransferListItem/test.tsx | 61 --- .../UserDetailsContent.spec.tsx | 65 +++ .../UserDetailsContent/UserDetailsContent.tsx | 11 +- .../UserModule/UserDetailsContent/test.tsx | 105 ----- .../UserListItem/UserListItem.spec.tsx | 37 ++ .../UserModule/UserListItem/UserListItem.tsx | 4 +- .../modules/UserModule/UserListItem/test.tsx | 60 --- .../modules/UserModule/UserModal/test.tsx | 123 ------ .../{test.tsx => WizardBreadcrumbs.spec.tsx} | 41 +- .../WizardBreadcrumbs/WizardBreadcrumbs.tsx | 4 +- .../WizardEndpointList.spec.tsx | 48 +++ .../WizardEndpointList/WizardEndpointList.tsx | 7 +- .../WizardModule/WizardEndpointList/test.tsx | 102 ----- .../WizardInstances/WizardInstances.spec.tsx | 48 +++ .../WizardInstances/WizardInstances.tsx | 20 +- .../WizardModule/WizardInstances/test.tsx | 154 ------- .../WizardNetworks/WizardNetworks.spec.tsx | 40 ++ .../WizardNetworks/WizardNetworks.tsx | 16 +- .../WizardModule/WizardNetworks/test.tsx | 114 ------ .../WizardOptions/WizardOptions.spec.tsx | 50 +++ .../WizardModule/WizardOptions/test.tsx | 138 ------- .../WizardPageContent/WizardPageContent.tsx | 38 +- .../WizardScripts/WizardScripts.spec.tsx | 41 ++ .../WizardScripts/WizardScripts.tsx | 15 +- .../WizardStorage/WizardStorage.spec.tsx | 43 ++ .../WizardStorage/WizardStorage.tsx | 18 +- .../WizardModule/WizardStorage/test.tsx | 150 ------- .../WizardSummary/WizardSummary.spec.tsx | 71 ++++ .../WizardModule/WizardSummary/test.tsx | 112 ----- .../WizardType/WizardType.spec.tsx | 35 ++ .../WizardModule/WizardType/WizardType.tsx | 5 +- .../modules/WizardModule/WizardType/test.tsx | 38 -- .../ui/AlertModal/AlertModal.spec.tsx | 188 +++++++-- src/components/ui/AutocompleteInput/test.tsx | 64 --- .../ActionDropdown/ActionDropdown.tsx | 9 +- src/components/ui/Logo/Logo.spec.tsx | 3 +- .../ui/SmallLoading/SmallLoading.tsx | 4 +- tests/TestUtils.ts | 33 +- tests/mocks/EndpointsMock.ts | 32 ++ tests/mocks/ExecutionsMock.ts | 36 ++ tests/mocks/InstancesMock.ts | 24 ++ tests/mocks/MetalHubServerMock.ts | 52 +++ tests/mocks/MinionPoolMock.ts | 83 ++++ tests/mocks/NetworksMock.ts | 8 + tests/mocks/ProvidersMock.ts | 50 +++ tests/mocks/SchedulesMock.ts | 15 + tests/mocks/StoragesMock.ts | 23 ++ tests/mocks/TransferMock.ts | 97 +++++ tests/mocks/UsersMock.ts | 17 + tests/setup.js | 15 +- tests/testCoverage.js | 62 --- yarn.lock | 43 +- 168 files changed, 8215 insertions(+), 3797 deletions(-) create mode 100644 src/components/modules/DashboardModule/DashboardContent/DashboardContent.spec.tsx create mode 100644 src/components/modules/DashboardModule/DashboardExecutions/DashboardExecutions.spec.tsx create mode 100644 src/components/modules/DashboardModule/DashboardInfoCount/DashboardInfoCount.spec.tsx create mode 100644 src/components/modules/DashboardModule/DashboardLicence/DashboardLicence.spec.tsx create mode 100644 src/components/modules/DashboardModule/DashboardPieChart/DashboardPieChart.spec.tsx create mode 100644 src/components/modules/DashboardModule/DashboardTopEndpoints/DashboardTopEndpoints.spec.tsx create mode 100644 src/components/modules/DetailsModule/DetailsContentHeader/DetailsContentHeader.spec.tsx delete mode 100644 src/components/modules/DetailsModule/DetailsContentHeader/test.tsx create mode 100644 src/components/modules/DetailsModule/DetailsPageHeader/DetailsPageHeader.spec.tsx delete mode 100644 src/components/modules/DetailsModule/DetailsPageHeader/test.tsx create mode 100644 src/components/modules/EndpointModule/ChooseProvider/ChooseProvider.spec.tsx create mode 100644 src/components/modules/EndpointModule/ChooseProvider/MultipleUploadedEndpoints.spec.tsx delete mode 100644 src/components/modules/EndpointModule/ChooseProvider/test.tsx create mode 100644 src/components/modules/EndpointModule/EndpointDetailsContent/EndpointDetailsContent.spec.tsx delete mode 100644 src/components/modules/EndpointModule/EndpointDetailsContent/test.tsx create mode 100644 src/components/modules/EndpointModule/EndpointDuplicateOptions/EndpointDuplicateOptions.spec.tsx delete mode 100644 src/components/modules/EndpointModule/EndpointDuplicateOptions/test.tsx create mode 100644 src/components/modules/EndpointModule/EndpointListItem/EndpointListItem.spec.tsx delete mode 100644 src/components/modules/EndpointModule/EndpointListItem/test.tsx create mode 100644 src/components/modules/EndpointModule/EndpointLogos/EndpointLogos.spec.tsx create mode 100644 src/components/modules/EndpointModule/EndpointLogos/resources/Generic.spec.tsx delete mode 100644 src/components/modules/EndpointModule/EndpointLogos/test.tsx create mode 100644 src/components/modules/EndpointModule/EndpointValidation/EndpointValidation.spec.tsx delete mode 100644 src/components/modules/EndpointModule/EndpointValidation/test.tsx create mode 100644 src/components/modules/LicenceModule/LicenceModule.spec.tsx create mode 100644 src/components/modules/LoginModule/LoginForm/LoginForm.spec.tsx delete mode 100644 src/components/modules/LoginModule/LoginForm/test.tsx delete mode 100644 src/components/modules/LoginModule/LoginFormField/test.tsx create mode 100644 src/components/modules/LoginModule/LoginOptions/LoginOptions.spec.tsx delete mode 100644 src/components/modules/LoginModule/LoginOptions/test.tsx create mode 100644 src/components/modules/MetalHubModule/MetalHubListHeader/MetalHubListHeader.spec.tsx create mode 100644 src/components/modules/MetalHubModule/MetalHubListItem/MetalHubListItem.spec.tsx create mode 100644 src/components/modules/MetalHubModule/MetalHubModal/MetalHubModal.spec.tsx create mode 100644 src/components/modules/MetalHubModule/MetalHubServerDetailsContent/MetalHubServerDetailsContent.spec.tsx create mode 100644 src/components/modules/MinionModule/MinionEndpointModal/MinionEndpointModal.spec.tsx create mode 100644 src/components/modules/MinionModule/MinionPoolConfirmationModal/MinionPoolConfirmationModal.spec.tsx create mode 100644 src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolDetailsContent.spec.tsx create mode 100644 src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolEvents.spec.tsx create mode 100644 src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolMachines.spec.tsx create mode 100644 src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolMainDetails.spec.tsx create mode 100644 src/components/modules/MinionModule/MinionPoolListItem/MinionPoolListItem.spec.tsx create mode 100644 src/components/modules/MinionModule/MinionPoolModal/MinionPoolModalContent.spec.tsx delete mode 100644 src/components/modules/NavigationModule/DetailsNavigation/test.tsx create mode 100644 src/components/modules/NavigationModule/NavigationMini/NavigationMini.spec.tsx create mode 100644 src/components/modules/ProjectModule/ProjectDetailsContent/ProjectDetailsContent.spec.tsx delete mode 100644 src/components/modules/ProjectModule/ProjectDetailsContent/test.tsx create mode 100644 src/components/modules/ProjectModule/ProjectListItem/ProjectListItem.spec.tsx delete mode 100644 src/components/modules/ProjectModule/ProjectListItem/test.tsx delete mode 100644 src/components/modules/ProjectModule/ProjectMemberModal/test.tsx delete mode 100644 src/components/modules/ProjectModule/ProjectModal/test.tsx create mode 100644 src/components/modules/SetupModule/SetupPageEmailBody/SetupPageEmailBody.spec.tsx rename src/components/modules/{NavigationModule/Navigation/test.tsx => SetupModule/SetupPageHelp/SetupPageHelp.spec.tsx} (53%) create mode 100644 src/components/modules/SetupModule/SetupPageLegal/SetupPageLegal.spec.tsx create mode 100644 src/components/modules/SetupModule/SetupPageLicence/SetupPageLicence.spec.tsx rename src/components/modules/{WizardModule/WizardPageContent/test.tsx => SetupModule/SetupPageModuleWrapper/SetupPageModuleWrapper.spec.tsx} (50%) rename src/components/modules/{NavigationModule/NavigationMini/test.tsx => SetupModule/SetupPageWelcome/SetupPageWelcome.spec.tsx} (56%) create mode 100644 src/components/modules/SetupModule/ui/SetupPageBackButton/SetupPageBackButton.spec.tsx create mode 100644 src/components/modules/SetupModule/ui/SetupPagePasswordStrength/SetupPagePasswordStrength.spec.tsx create mode 100644 src/components/modules/TemplateModule/DetailsTemplate/DetailsTemplate.spec.tsx create mode 100644 src/components/modules/TemplateModule/EmptyTemplate/EmptyTemplate.spec.tsx create mode 100644 src/components/modules/TemplateModule/MainTemplate/MainTemplate.spec.tsx create mode 100644 src/components/modules/TemplateModule/WizardTemplate/WizardTemplate.spec.tsx create mode 100644 src/components/modules/TransferModule/DeleteReplicaModal/DeleteReplicaModal.spec.tsx create mode 100644 src/components/modules/TransferModule/Executions/Executions.spec.tsx delete mode 100644 src/components/modules/TransferModule/Executions/test.tsx create mode 100644 src/components/modules/TransferModule/MainDetails/MainDetails.spec.tsx delete mode 100644 src/components/modules/TransferModule/MainDetails/test.tsx create mode 100644 src/components/modules/TransferModule/MigrationDetailsContent/MigrationDetailsContent.spec.tsx delete mode 100644 src/components/modules/TransferModule/MigrationDetailsContent/test.tsx create mode 100644 src/components/modules/TransferModule/ReplicaDetailsContent/ReplicaDetailsContent.spec.tsx delete mode 100644 src/components/modules/TransferModule/ReplicaDetailsContent/test.tsx create mode 100644 src/components/modules/TransferModule/ReplicaExecutionOptions/ReplicaExecutionOptions.spec.tsx delete mode 100644 src/components/modules/TransferModule/ReplicaExecutionOptions/test.tsx create mode 100644 src/components/modules/TransferModule/ReplicaMigrationOptions/ReplicaMigrationOptions.spec.tsx delete mode 100644 src/components/modules/TransferModule/ReplicaMigrationOptions/test.tsx create mode 100644 src/components/modules/TransferModule/Schedule/Schedule.spec.tsx delete mode 100644 src/components/modules/TransferModule/Schedule/test.tsx create mode 100644 src/components/modules/TransferModule/ScheduleItem/ScheduleItem.spec.tsx delete mode 100644 src/components/modules/TransferModule/ScheduleItem/test.tsx create mode 100644 src/components/modules/TransferModule/TaskItem/TaskItem.spec.tsx delete mode 100644 src/components/modules/TransferModule/TaskItem/test.tsx create mode 100644 src/components/modules/TransferModule/Tasks/Tasks.spec.tsx delete mode 100644 src/components/modules/TransferModule/Tasks/test.tsx delete mode 100644 src/components/modules/TransferModule/Timeline/test.tsx create mode 100644 src/components/modules/TransferModule/TransferDetailsTable/TransferDetailsTable.spec.tsx delete mode 100644 src/components/modules/TransferModule/TransferDetailsTable/test.tsx create mode 100644 src/components/modules/TransferModule/TransferListItem/TransferListItem.spec.tsx delete mode 100644 src/components/modules/TransferModule/TransferListItem/test.tsx create mode 100644 src/components/modules/UserModule/UserDetailsContent/UserDetailsContent.spec.tsx delete mode 100644 src/components/modules/UserModule/UserDetailsContent/test.tsx create mode 100644 src/components/modules/UserModule/UserListItem/UserListItem.spec.tsx delete mode 100644 src/components/modules/UserModule/UserListItem/test.tsx delete mode 100644 src/components/modules/UserModule/UserModal/test.tsx rename src/components/modules/WizardModule/WizardBreadcrumbs/{test.tsx => WizardBreadcrumbs.spec.tsx} (52%) create mode 100644 src/components/modules/WizardModule/WizardEndpointList/WizardEndpointList.spec.tsx delete mode 100644 src/components/modules/WizardModule/WizardEndpointList/test.tsx create mode 100644 src/components/modules/WizardModule/WizardInstances/WizardInstances.spec.tsx delete mode 100644 src/components/modules/WizardModule/WizardInstances/test.tsx create mode 100644 src/components/modules/WizardModule/WizardNetworks/WizardNetworks.spec.tsx delete mode 100644 src/components/modules/WizardModule/WizardNetworks/test.tsx create mode 100644 src/components/modules/WizardModule/WizardOptions/WizardOptions.spec.tsx delete mode 100644 src/components/modules/WizardModule/WizardOptions/test.tsx create mode 100644 src/components/modules/WizardModule/WizardScripts/WizardScripts.spec.tsx create mode 100644 src/components/modules/WizardModule/WizardStorage/WizardStorage.spec.tsx delete mode 100644 src/components/modules/WizardModule/WizardStorage/test.tsx create mode 100644 src/components/modules/WizardModule/WizardSummary/WizardSummary.spec.tsx delete mode 100644 src/components/modules/WizardModule/WizardSummary/test.tsx create mode 100644 src/components/modules/WizardModule/WizardType/WizardType.spec.tsx delete mode 100644 src/components/modules/WizardModule/WizardType/test.tsx delete mode 100644 src/components/ui/AutocompleteInput/test.tsx create mode 100644 tests/mocks/EndpointsMock.ts create mode 100644 tests/mocks/ExecutionsMock.ts create mode 100644 tests/mocks/InstancesMock.ts create mode 100644 tests/mocks/MetalHubServerMock.ts create mode 100644 tests/mocks/MinionPoolMock.ts create mode 100644 tests/mocks/NetworksMock.ts create mode 100644 tests/mocks/ProvidersMock.ts create mode 100644 tests/mocks/SchedulesMock.ts create mode 100644 tests/mocks/StoragesMock.ts create mode 100644 tests/mocks/TransferMock.ts create mode 100644 tests/mocks/UsersMock.ts delete mode 100644 tests/testCoverage.js diff --git a/.gitignore b/.gitignore index 4a2fc8fb..13ce74ae 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ cypress/videos !.yarn/releases !.yarn/sdks !.yarn/versions + + +# testing +coverage diff --git a/jest.config.ts b/jest.config.ts index 8dcf0a35..9adf0b6d 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -17,10 +17,29 @@ export default { clearMocks: true, // Indicates whether the coverage information should be collected while executing the test - // collectCoverage: false, + collectCoverage: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - // collectCoverageFrom: undefined, + collectCoverageFrom: [ + "**/*.tsx", // Include all .tsx files + "!**/AssessmentModule/**", // Exclude files within the AssessmentModule directory (this is a module that is not used in the app) + "!**/story.tsx", // Exclude all storybook files + "!**/test.tsx", // Exclude old test files + "!**/plugins/**", // Exclude files within the plugins directory + "!src/index.tsx", // Exclude the index.tsx file + "!**/App.tsx", // Exclude the App.tsx file + "!**/smart/**", // Exclude files within the smart directory (this is a directory that contains containers) + // other smart components + "!**/EndpointModal.tsx", + "!**/MinionPoolModal.tsx", + "!**/TransferItemModal.tsx", + "!**/Navigation.tsx", + "!**/NotificationsModule.tsx", + "!**/ProjectModal.tsx", + "!**/ProjectMemberModal.tsx", + "!**/UserModal.tsx", + "!**/WizardPageContent.tsx", + ], // The directory where Jest should output its coverage files // coverageDirectory: undefined, @@ -31,15 +50,10 @@ export default { // ], // Indicates which provider should be used to instrument code for coverage - coverageProvider: "v8", + // coverageProvider: "babel", // A list of reporter names that Jest uses when writing coverage reports - // coverageReporters: [ - // "json", - // "text", - // "lcov", - // "clover" - // ], + coverageReporters: ["html", "text-summary"], // An object that configures minimum threshold enforcement for coverage results // coverageThreshold: undefined, diff --git a/package.json b/package.json index 2b7b6b6a..80d77418 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "test": "jest", "e2e": "cypress run", "test-release": "node ./tests/testRelease", - "test-coverage": "node ./tests/testCoverage", "storybook": "start-storybook" }, "devDependencies": { @@ -31,7 +30,7 @@ "@types/file-saver": "^2.0.1", "@types/jest": "^27.0.2", "@types/js-cookie": "^2.2.6", - "@types/luxon": "^3.3.2", + "@types/luxon": "^3.3.3", "@types/moment-timezone": "^0.5.13", "@types/react": "^16.13.1", "@types/react-collapse": "^5.0.0", @@ -51,6 +50,7 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.31.7", "jest": "^27.3.1", + "jest-canvas-mock": "^2.5.2", "nodemon": "^2.0.4", "prettier": "^2.7.1" }, diff --git a/src/components/modules/DashboardModule/DashboardBarChart/DashboardBarChart.spec.tsx b/src/components/modules/DashboardModule/DashboardBarChart/DashboardBarChart.spec.tsx index ad571fc8..af18dc26 100644 --- a/src/components/modules/DashboardModule/DashboardBarChart/DashboardBarChart.spec.tsx +++ b/src/components/modules/DashboardModule/DashboardBarChart/DashboardBarChart.spec.tsx @@ -13,11 +13,13 @@ along with this program. If not, see . */ import React from "react"; -import { render } from "@testing-library/react"; -import TestUtils from "@tests/TestUtils"; + import { ThemePalette } from "@src/components/Theme"; +import { render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import DashboardBarChart from "."; +import TestUtils from "@tests/TestUtils"; + +import DashboardBarChart from "./"; const DATA: DashboardBarChart["props"]["data"] = [ { @@ -109,4 +111,139 @@ describe("DashboardBarChart", () => { ); } ); + + it("does not render bars with height of 0%", () => { + const ZERO_DATA = [ + { + label: "label 1", + values: [0, 0], + }, + { + label: "label 2", + values: [20, 25], + }, + ]; + + render(); + + const firstStackedBars = TestUtils.selectAll( + "DashboardBarChart__StackedBar-", + TestUtils.selectAll("DashboardBarChart__Bar-")[0] + ); + const secondStackedBars = TestUtils.selectAll( + "DashboardBarChart__StackedBar-", + TestUtils.selectAll("DashboardBarChart__Bar-")[1] + ); + + expect(firstStackedBars.length).toBe(0); + expect(secondStackedBars.length).toBe(ZERO_DATA[1].values.length); + }); + + it("renders half the bars if available width is less than 30 times the number of items", () => { + const originalInnerWidth = window.innerWidth; + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 29 * DATA.length, + }); + + render(); + + const bars = TestUtils.selectAll("DashboardBarChart__Bar-"); + + expect(bars.length).toBe(DATA.length / 2); + + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: originalInnerWidth, + }); + }); + + it("fires the onBarMouseLeave callback on bar mouse leave", () => { + const onBarMouseLeave = jest.fn(); + + render( + + ); + + const bar = TestUtils.selectAll("DashboardBarChart__StackedBar-")[0]; + userEvent.unhover(bar); + + expect(onBarMouseLeave).toHaveBeenCalled(); + }); + + it("calculates the correct position for bars", () => { + const onBarMouseEnter = jest.fn(); + render( + + ); + + const firstBar = TestUtils.selectAll("DashboardBarChart__StackedBar-")[0]; + userEvent.hover(firstBar); + + expect(onBarMouseEnter).toHaveBeenCalledWith({ x: 48, y: 65 }, DATA[0]); + }); + + it("recalculates ticks when new data is received", () => { + const { rerender } = render( + + ); + + const bars = TestUtils.selectAll("DashboardBarChart__Bar-"); + expect(bars.length).toBe(DATA.length); + expect(bars[0].textContent).toBe("label 1"); + expect(bars[1].textContent).toBe("label 2"); + + const NEW_DATA = [ + { + label: "label 3", + values: [10, 30], + data: "data 3", + }, + { + label: "label 4", + values: [5, 20], + data: "data 4", + }, + ]; + + // Mocking the offset width is necessary due to how the rendered + // output behaves within the @testing-library/react environment + Object.defineProperty(HTMLElement.prototype, "offsetWidth", { + configurable: true, + value: 500, + }); + rerender(); + + const newBars = TestUtils.selectAll("DashboardBarChart__Bar-"); + expect(newBars.length).toBe(NEW_DATA.length); + expect(newBars[0].textContent).toBe("label 3"); + expect(newBars[1].textContent).toBe("label 4"); + }); + + it("does not fire any function when onBarMouseEnter is not provided", () => { + render(); + + const firstStackedBar = TestUtils.selectAll( + "DashboardBarChart__StackedBar-" + )[0]; + const spy = jest.spyOn(console, "error").mockImplementation(() => {}); + + // Hover over the stacked bar + userEvent.hover(firstStackedBar); + + // Assert that there were no console errors + expect(spy).not.toHaveBeenCalled(); + + spy.mockRestore(); + }); }); diff --git a/src/components/modules/DashboardModule/DashboardContent/DashboardContent.spec.tsx b/src/components/modules/DashboardModule/DashboardContent/DashboardContent.spec.tsx new file mode 100644 index 00000000..5d0cee24 --- /dev/null +++ b/src/components/modules/DashboardModule/DashboardContent/DashboardContent.spec.tsx @@ -0,0 +1,131 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { act, render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import DashboardContent from "./DashboardContent"; + +jest.mock("react-router-dom", () => ({ Link: "div" })); + +describe("DashboardContent", () => { + let resizeWindow: (x: number, y: number) => void; + let defaultProps: DashboardContent["props"]; + + beforeAll(() => { + resizeWindow = (x, y) => { + window.innerWidth = x; + window.innerHeight = y; + window.dispatchEvent(new Event("resize")); + }; + }); + + beforeEach(() => { + defaultProps = { + replicas: [], + migrations: [], + endpoints: [], + projects: [], + replicasLoading: false, + migrationsLoading: false, + endpointsLoading: false, + usersLoading: false, + projectsLoading: false, + licenceLoading: false, + notificationItemsLoading: false, + users: [], + licence: null, + licenceServerStatus: null, + licenceError: null, + notificationItems: [], + isAdmin: false, + onNewReplicaClick: jest.fn(), + onNewEndpointClick: jest.fn(), + onAddLicenceClick: jest.fn(), + }; + }); + + it("renders modules for non-admin users", () => { + render(); + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockLabel") + ).toHaveLength(3); + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockLabel")[0].textContent + ).toBe("Replicas"); + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockLabel")[1].textContent + ).toBe("Migrations"); + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockLabel")[2].textContent + ).toBe("Endpoints"); + }); + + it("renders additional modules for admin users", () => { + render(); + + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockLabel") + ).toHaveLength(5); + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockLabel")[0].textContent + ).toBe("Replicas"); + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockLabel")[1].textContent + ).toBe("Migrations"); + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockLabel")[2].textContent + ).toBe("Endpoints"); + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockLabel")[3].textContent + ).toBe("Users"); + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockLabel")[4].textContent + ).toBe("Projects"); + }); + + it("switches to mobile layout when window width is less than 1120", () => { + resizeWindow(1100, 800); + render(); + + expect( + TestUtils.select("DashboardContent__MiddleMobileLayout") + ).toBeTruthy(); + }); + + it("handleResize updates state correctly based on window size", async () => { + resizeWindow(2400, 800); + + let instance: DashboardContent | null = null; + + const setRef = (componentInstance: DashboardContent) => { + instance = componentInstance; + }; + + render(); + + const setStateMock = jest.spyOn(instance!, "setState"); + + act(() => { + resizeWindow(1000, 800); + }); + + expect(setStateMock).toHaveBeenCalledWith({ useMobileLayout: true }); + + setStateMock.mockRestore(); + resizeWindow(2400, 800); + }); +}); diff --git a/src/components/modules/DashboardModule/DashboardExecutions/DashboardExecutions.spec.tsx b/src/components/modules/DashboardModule/DashboardExecutions/DashboardExecutions.spec.tsx new file mode 100644 index 00000000..ee2d869e --- /dev/null +++ b/src/components/modules/DashboardModule/DashboardExecutions/DashboardExecutions.spec.tsx @@ -0,0 +1,252 @@ +/* +Copyright (C) 2023 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 . +*/ + +import { DateTime } from "luxon"; +import React from "react"; + +import { MigrationItem, ReplicaItem } from "@src/@types/MainItem"; +import { render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import TestUtils from "@tests/TestUtils"; + +import DashboardExecutions from "./DashboardExecutions"; + +type BuildType = T extends "replica" + ? ReplicaItem + : MigrationItem; + +const buildItem = ( + type: T, + date: string +): BuildType => { + const item = { + id: "", + type, + name: "", + created_at: date, + updated_at: date, + origin_endpoint_id: "", + destination_endpoint_id: "", + notes: "", + origin_minion_pool_id: null, + destination_minion_pool_id: null, + instances: [""], + info: {}, + destination_environment: {}, + source_environment: {}, + transfer_result: null, + last_execution_status: "", + user_id: "", + }; + return item as BuildType; +}; +const now = DateTime.utc(); +const TWENTIETH = DateTime.utc(now.year, now.month, 20, 10, 0); +const replicas: DashboardExecutions["props"]["replicas"] = [ + buildItem("replica", TWENTIETH.minus({ days: 5 }).toISO()!), + buildItem("replica", TWENTIETH.toISO()!), +]; + +const migrations: DashboardExecutions["props"]["migrations"] = [ + buildItem("migration", TWENTIETH.toISO()!), + buildItem("migration", TWENTIETH.minus({ months: 2 }).toISO()!), +]; + +describe("DashboardExecutions", () => { + let defaultProps: DashboardExecutions["props"]; + + beforeEach(() => { + defaultProps = { + replicas, + migrations, + loading: false, + }; + }); + + it("shows no recent activity message", () => { + const newProps = { + ...defaultProps, + replicas: [], + migrations: [], + }; + render(); + + expect(TestUtils.select("DashboardExecutions__Title")?.textContent).toBe( + "Items Created" + ); + expect( + TestUtils.select("DashboardExecutions__NoDataMessage")?.textContent + ).toBe("No recent activity in this project"); + }); + + it("groups data correctly", () => { + render(); + expect( + TestUtils.select("DashboardExecutions__BarChartWrapper") + ).toBeTruthy(); + expect(TestUtils.selectAll("DashboardBarChart__Bar-")).toHaveLength(2); + expect( + TestUtils.selectAll( + "DashboardBarChart__StackedBar-", + TestUtils.selectAll("DashboardBarChart__Bar-")[0] + ) + ).toHaveLength(1); + expect( + TestUtils.selectAll( + "DashboardBarChart__StackedBar-", + TestUtils.selectAll("DashboardBarChart__Bar-")[1] + ) + ).toHaveLength(2); + expect(TestUtils.select("DropdownLink__Label")?.textContent).toBe( + "Last 30 days" + ); + expect( + TestUtils.selectAll("DashboardBarChart__BarLabel")[0].textContent + ).toBe(TWENTIETH.minus({ days: 5 }).toFormat("dd LLL")); + expect( + TestUtils.selectAll("DashboardBarChart__BarLabel")[1].textContent + ).toBe(TWENTIETH.toFormat("dd LLL")); + }); + + it("updates period and regroups data when dropdown is changed", async () => { + // Mocking the offset width is necessary due to how the rendered + // output behaves within the @testing-library/react environment + Object.defineProperty(HTMLElement.prototype, "offsetWidth", { + configurable: true, + value: 500, + }); + + render(); + + userEvent.click(TestUtils.select("DropdownLink__LinkButton")!); + expect(TestUtils.selectAll("DropdownLink__ListItem-")[1].textContent).toBe( + "Last 12 months" + ); + userEvent.click(TestUtils.selectAll("DropdownLink__ListItem-")[1]!); + expect(TestUtils.select("DropdownLink__Label")?.textContent).toBe( + "Last 12 months" + ); + expect(TestUtils.selectAll("DashboardBarChart__Bar-")).toHaveLength(2); + expect( + TestUtils.selectAll( + "DashboardBarChart__StackedBar-", + TestUtils.selectAll("DashboardBarChart__Bar-")[0] + ) + ).toHaveLength(1); + expect( + TestUtils.selectAll( + "DashboardBarChart__StackedBar-", + TestUtils.selectAll("DashboardBarChart__Bar-")[1] + ) + ).toHaveLength(2); + expect( + TestUtils.selectAll("DashboardBarChart__BarLabel")[0].textContent + ).toBe(TWENTIETH.minus({ months: 2 }).toFormat("LLL")); + expect( + TestUtils.selectAll("DashboardBarChart__BarLabel")[1].textContent + ).toBe(TWENTIETH.toFormat("LLL")); + }); + + it("shows tooltip correctly on bar hover", () => { + // Mocking the offset width is necessary due to how the rendered + // output behaves within the @testing-library/react environment + Object.defineProperty(HTMLElement.prototype, "offsetWidth", { + configurable: true, + value: 500, + }); + render(); + + userEvent.hover(TestUtils.select("DashboardBarChart__StackedBar-")!); + + expect(TestUtils.select("DashboardExecutions__Tooltip")).toBeTruthy(); + expect( + TestUtils.select("DashboardExecutions__TooltipHeader")?.textContent + ).toBe(TWENTIETH.minus({ days: 5 }).toFormat("dd LLLL")); + expect( + TestUtils.selectAll("DashboardExecutions__TooltipRow-")[0].textContent + ).toBe("Created1"); + expect( + TestUtils.selectAll("DashboardExecutions__TooltipRow-")[1].textContent + ).toBe("Replicas1"); + expect( + TestUtils.selectAll("DashboardExecutions__TooltipRow-")[2].textContent + ).toBe("Migrations0"); + + userEvent.unhover(TestUtils.select("DashboardBarChart__StackedBar-")!); + expect(TestUtils.select("DashboardExecutions__Tooltip")).toBeFalsy(); + }); + + it("renders correct child based on state and props", () => { + // Mocking the offset width is necessary due to how the rendered + // output behaves within the @testing-library/react environment + Object.defineProperty(HTMLElement.prototype, "offsetWidth", { + configurable: true, + value: 500, + }); + const { rerender } = render(); + expect(TestUtils.selectAll("DashboardBarChart__Bar-")).toHaveLength(2); + + const newProps = { + ...defaultProps, + replicas: [], + }; + rerender(); + expect(TestUtils.selectAll("DashboardBarChart__Bar-")).toHaveLength(1); + expect(TestUtils.select("DashboardExecutions__NoDataMessage")).toBeFalsy(); + + const newProps2 = { + ...defaultProps, + migrations: [], + replicas: [], + }; + rerender(); + expect(TestUtils.select("DashboardExecutions__NoDataMessage")).toBeTruthy(); + }); + + it("shows loading state when loading prop is true and there are no replicas", () => { + const newProps = { + ...defaultProps, + loading: true, + }; + const { rerender } = render(); + + expect(TestUtils.select("DashboardExecutions__LoadingWrapper")).toBeFalsy(); + + const newProps2 = { + ...defaultProps, + loading: true, + replicas: [], + }; + rerender(); + + expect( + TestUtils.select("DashboardExecutions__LoadingWrapper") + ).toBeTruthy(); + }); + + it("shows no bar if item is not of type replica or migration", () => { + const newProps: DashboardExecutions["props"] = { + ...defaultProps, + replicas: [ + { + ...replicas[0], + // @ts-expect-error + type: "invalid", + }, + ], + }; + render(); + expect(TestUtils.selectAll("DashboardBarChart__Bar-")).toHaveLength(1); + }); +}); diff --git a/src/components/modules/DashboardModule/DashboardInfoCount/DashboardInfoCount.spec.tsx b/src/components/modules/DashboardModule/DashboardInfoCount/DashboardInfoCount.spec.tsx new file mode 100644 index 00000000..522fe56f --- /dev/null +++ b/src/components/modules/DashboardModule/DashboardInfoCount/DashboardInfoCount.spec.tsx @@ -0,0 +1,76 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import DashboardInfoCount from "./DashboardInfoCount"; + +jest.mock("react-router-dom", () => ({ Link: "a" })); + +describe("DashboardInfoCount", () => { + const mockData = [ + { + label: "Label1", + value: 1, + color: "red", + link: "/link1", + loading: false, + }, + { + label: "Label2", + value: 0, + color: "blue", + link: "/link2", + loading: true, + }, + ]; + + it("renders CountBlock for each data item", () => { + render(); + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockValue-")[0].textContent + ).toBe("1"); + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockLabel-")[0].textContent + ).toBe("Label1"); + expect( + TestUtils.selectAll("DashboardInfoCount__CountBlockLabel-")[1].textContent + ).toBe("Label2"); + // In this case, the value "0" will not be rendered because of the loading state. + }); + + it("renders loading state when item.loading is true and item.value is falsy", () => { + render(); + + expect( + TestUtils.select( + "DashboardInfoCount__LoadingWrapper-", + TestUtils.selectAll("DashboardInfoCount__CountBlock-")[1] + ) + ).toBeTruthy(); + }); + + it("renders CountBlockLabel with the correct color and link", () => { + render(); + const labels = TestUtils.selectAll("DashboardInfoCount__CountBlockLabel-"); + + expect(window.getComputedStyle(labels[0]).color).toBe("red"); + expect(window.getComputedStyle(labels[1]).color).toBe("blue"); + expect(labels[0].attributes.getNamedItem("to")!.value).toBe("/link1"); + expect(labels[1].attributes.getNamedItem("to")!.value).toBe("/link2"); + }); +}); diff --git a/src/components/modules/DashboardModule/DashboardLicence/DashboardLicence.spec.tsx b/src/components/modules/DashboardModule/DashboardLicence/DashboardLicence.spec.tsx new file mode 100644 index 00000000..d48aa2c3 --- /dev/null +++ b/src/components/modules/DashboardModule/DashboardLicence/DashboardLicence.spec.tsx @@ -0,0 +1,181 @@ +/* +Copyright (C) 2023 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 . +*/ + +import { DateTime } from "luxon"; +import React from "react"; + +import { Licence, LicenceServerStatus } from "@src/@types/Licence"; +import { render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import DashboardLicence from "./DashboardLicence"; + +describe("DashboardLicence", () => { + const futureLicence: Licence = { + applianceId: "test-id", + earliestLicenceExpiryDate: DateTime.now().plus({ years: 1 }).toJSDate(), + latestLicenceExpiryDate: DateTime.now().plus({ years: 1 }).toJSDate(), + currentPerformedReplicas: 5, + currentPerformedMigrations: 3, + lifetimePerformedMigrations: 4, + lifetimePerformedReplicas: 6, + currentAvailableReplicas: 10, + currentAvailableMigrations: 5, + lifetimeAvailableReplicas: 15, + lifetimeAvailableMigrations: 10, + }; + + const status: LicenceServerStatus = { + hostname: "test-hostname", + multi_appliance: false, + supported_licence_versions: ["v2"], + server_local_time: DateTime.now().toISO()!, + }; + + let defaultProps: DashboardLicence["props"]; + + beforeEach(() => { + defaultProps = { + licence: futureLicence, + licenceServerStatus: status, + licenceError: null, + loading: false, + onAddClick: jest.fn(), + }; + }); + + it("renders licence status text when licence and licenceServerStatus are provided and the licence has not expired", () => { + render(); + const futureDate = DateTime.now().plus({ years: 1 }); + expect( + TestUtils.select("DashboardLicence__TopInfoDateTop-")?.textContent + ).toBe(`${futureDate.toFormat("LLL")} '${futureDate.toFormat("yy")}`); + expect( + TestUtils.select("DashboardLicence__TopInfoDateBottom-")?.textContent + ).toBe(futureDate.toFormat("dd")); + + expect( + TestUtils.selectAll("DashboardLicence__ChartHeaderCurrent-")[0] + .textContent + ).toBe("5 Used Replicas "); + expect( + TestUtils.selectAll("DashboardLicence__ChartHeaderTotal-")[0].textContent + ).toBe("Total 10"); + expect( + TestUtils.selectAll("DashboardLicence__ChartHeaderCurrent-")[1] + .textContent + ).toBe("3 Used Migrations "); + expect( + TestUtils.selectAll("DashboardLicence__ChartHeaderTotal-")[1].textContent + ).toBe("Total 5"); + }); + + it("renders licence error when licenceError prop is provided and there's no licence", () => { + const newProps = { + ...defaultProps, + licence: null, + licenceError: "An error occurred.", + }; + render(); + + expect(TestUtils.select("DashboardLicence__LicenceError-")).toBeTruthy(); + expect( + TestUtils.select("DashboardLicence__LicenceError-")?.textContent + ).toBe("An error occurred."); + }); + + it("renders expired licence details when licence has expired", () => { + const newProps = { + ...defaultProps, + licence: { + ...futureLicence, + earliestLicenceExpiryDate: DateTime.now().minus({ days: 2 }).toJSDate(), + }, + }; + + render(); + + expect(TestUtils.select("DashboardLicence__LicenceError-")).toBeTruthy(); + expect( + TestUtils.select("DashboardLicence__LicenceError-")?.textContent + ).toContain("Please contact Cloudbase Solutions with your Appliance ID"); + expect( + TestUtils.select("DashboardLicence__ApplianceId-")?.textContent + ).toBe("Appliance ID:test-id-licencev2"); + expect(TestUtils.select("Button__")?.textContent).toBe("Add Licence"); + }); + + it("renders loading status when loading prop is true and there's no licence", () => { + const newProps = { + ...defaultProps, + licence: null, + loading: true, + }; + render(); + expect(TestUtils.select("StatusImage__Wrapper-")).toBeTruthy(); + }); + + it("does not render anything when no props are provided", () => { + const newProps = { + ...defaultProps, + licence: null, + }; + render(); + expect(document.body.innerHTML).toBe("
"); + }); + + it("hides licenceLogoRef if buttonWrapperRef width is less than 370", async () => { + const newProps = { + ...defaultProps, + licence: { + ...futureLicence, + earliestLicenceExpiryDate: DateTime.now().minus({ days: 2 }).toJSDate(), + }, + }; + render(); + + const button = TestUtils.select( + "DashboardLicence__AddLicenceButtonWrapper" + ); + const logo = TestUtils.select("DashboardLicence__Logo"); + + button!.getBoundingClientRect = jest.fn().mockReturnValue({ width: 400 }); + window.dispatchEvent(new Event("resize")); + + expect(button).toBeTruthy(); + expect(logo).toBeTruthy(); + expect(logo!.style.display).toBe("block"); + + button!.getBoundingClientRect = jest.fn().mockReturnValue({ width: 360 }); + window.dispatchEvent(new Event("resize")); + + expect(logo!.style.display).toBe("none"); + }); + + it("renders singular label when current is 1", () => { + const newProps = { + ...defaultProps, + licence: { + ...futureLicence, + currentPerformedReplicas: 1, + }, + }; + + render(); + expect( + TestUtils.selectAll("DashboardLicence__ChartHeaderCurrent-")[0] + .textContent + ).toBe("1 Used Replica "); + }); +}); diff --git a/src/components/modules/DashboardModule/DashboardLicence/DashboardLicence.tsx b/src/components/modules/DashboardModule/DashboardLicence/DashboardLicence.tsx index 3b0e76a9..8be0c700 100644 --- a/src/components/modules/DashboardModule/DashboardLicence/DashboardLicence.tsx +++ b/src/components/modules/DashboardModule/DashboardLicence/DashboardLicence.tsx @@ -156,7 +156,7 @@ type Props = { licence: Licence | null; licenceServerStatus: LicenceServerStatus | null; loading: boolean; - style: any; + style?: React.CSSProperties; licenceError: string | null; onAddClick: () => void; }; diff --git a/src/components/modules/DashboardModule/DashboardPieChart/DashboardPieChart.spec.tsx b/src/components/modules/DashboardModule/DashboardPieChart/DashboardPieChart.spec.tsx new file mode 100644 index 00000000..2a3d0956 --- /dev/null +++ b/src/components/modules/DashboardModule/DashboardPieChart/DashboardPieChart.spec.tsx @@ -0,0 +1,295 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { fireEvent, render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import DashboardPieChart from "./DashboardPieChart"; + +describe("DashboardPieChart", () => { + const fireMouseMove = ( + element: HTMLElement, + options: MouseEventInit & { + offsetX?: number; + offsetY?: number; + } = {} + ) => { + const mouseMoveEvent = new MouseEvent("mousemove", { + bubbles: true, + cancelable: true, + ...options, + }); + Object.assign(mouseMoveEvent, { + offsetX: options.offsetX, + offsetY: options.offsetY, + }); + element.dispatchEvent(mouseMoveEvent); + }; + + it("calls drawChart on mount", () => { + const spy = jest.spyOn(DashboardPieChart.prototype, "drawChart"); + render(); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it("adds and removes event listeners on mount/unmount", () => { + const spyAdd = jest.spyOn(HTMLCanvasElement.prototype, "addEventListener"); + const spyRemove = jest.spyOn( + HTMLCanvasElement.prototype, + "removeEventListener" + ); + const { unmount } = render( + + ); + expect(spyAdd).toHaveBeenCalledWith("mousemove", expect.any(Function)); + expect(spyAdd).toHaveBeenCalledWith("mouseleave", expect.any(Function)); + unmount(); + expect(spyRemove).toHaveBeenCalledWith("mousemove", expect.any(Function)); + expect(spyRemove).toHaveBeenCalledWith("mouseleave", expect.any(Function)); + }); + + it("handleMouseMove triggers onMouseOver if item is detected", () => { + const onMouseOverMock = jest.fn(); + jest + .spyOn(CanvasRenderingContext2D.prototype, "isPointInPath") + .mockReturnValue(true); + + render( + + ); + const canvas = document.querySelector("canvas") as HTMLCanvasElement; + fireMouseMove(canvas, { offsetX: 50, offsetY: 50 }); + expect(onMouseOverMock).toHaveBeenCalled(); + }); + + it("handleMouseMove triggers onMouseLeave if no item is detected and has mouseOver", () => { + const onMouseLeaveMock = jest.fn(); + jest + .spyOn(CanvasRenderingContext2D.prototype, "isPointInPath") + .mockReturnValue(false); + + render( + {}} + /> + ); + const canvas = document.querySelector("canvas") as HTMLCanvasElement; + fireMouseMove(canvas, { offsetX: 50, offsetY: 50 }); + expect(onMouseLeaveMock).toHaveBeenCalled(); + }); + + it("doesn't throw if onMouseLeave is not provided", () => { + jest + .spyOn(CanvasRenderingContext2D.prototype, "isPointInPath") + .mockReturnValue(false); + + render( + {}} + /> + ); + const canvas = document.querySelector("canvas") as HTMLCanvasElement; + fireEvent.mouseLeave(canvas); + }); + + it("handleMouseLeave triggers onMouseLeave", () => { + const onMouseLeaveMock = jest.fn(); + render( + + ); + const canvas = document.querySelector("canvas") as HTMLCanvasElement; + fireEvent.mouseLeave(canvas); + expect(onMouseLeaveMock).toHaveBeenCalled(); + }); + + it("drawChart is called when props are updated", () => { + const { rerender } = render( + + ); + const spy = jest.spyOn(DashboardPieChart.prototype, "drawChart"); + rerender( + + ); + expect(spy).toHaveBeenCalled(); + }); + + it("renders Canvas, OuterShadow, and InnerShadow if holeStyle is provided", () => { + render( + + ); + expect(document.querySelector("canvas")).toBeTruthy(); + expect(TestUtils.select("DashboardPieChart__OuterShadow")).toBeTruthy(); + expect(TestUtils.select("DashboardPieChart__InnerShadow")).toBeTruthy(); + }); + + it("renders Canvas and OuterShadow without InnerShadow if holeStyle is not provided", () => { + render(); + + expect(document.querySelector("canvas")).toBeTruthy(); + expect(TestUtils.select("DashboardPieChart__OuterShadow")).toBeTruthy(); + expect(TestUtils.select("DashboardPieChart__InnerShadow")).toBeFalsy(); + }); + + it("does not add event listeners when canvas is null", () => { + const spyAdd = jest.spyOn(HTMLCanvasElement.prototype, "addEventListener"); + + const instance = new DashboardPieChart({ size: 100, data: [], colors: [] }); + instance.canvas = null; + instance.componentDidMount(); + + expect(spyAdd).not.toHaveBeenCalled(); + }); + + it("does not remove event listeners when canvas is null", () => { + const spyRemove = jest.spyOn( + HTMLCanvasElement.prototype, + "removeEventListener" + ); + + const instance = new DashboardPieChart({ size: 100, data: [], colors: [] }); + instance.canvas = null; + instance.componentWillUnmount(); + + expect(spyRemove).not.toHaveBeenCalled(); + }); + + it("does not attempt to draw on the canvas when canvas is null", () => { + const instance = new DashboardPieChart({ size: 100, data: [], colors: [] }); + instance.canvas = null; + const ctxSpy = jest.spyOn(HTMLCanvasElement.prototype, "getContext"); + + instance.drawChart(); + + expect(ctxSpy).not.toHaveBeenCalled(); + }); + + it("does not detect hits when canvas is null", () => { + const instance = new DashboardPieChart({ size: 100, data: [], colors: [] }); + instance.canvas = null; + const result = instance.detectHit(50, 50); + expect(result).toBeNull(); + }); + + it("does not handle mouse move if there's no mouse over", () => { + const detectHit = jest.spyOn(DashboardPieChart.prototype, "detectHit"); + + render(); + const canvas = document.querySelector("canvas") as HTMLCanvasElement; + fireMouseMove(canvas, { offsetX: 50, offsetY: 50 }); + expect(detectHit).not.toHaveBeenCalled(); + }); + + it("does not evenly divide angles when sum is not 0", () => { + const data = [{ value: 50 }, { value: 30 }, { value: 20 }]; + const component = new DashboardPieChart({ + size: 100, + data, + colors: ["#FFF", "#000", "#AAA"], + }); + component.canvas = document.createElement("canvas"); + component.drawChart(); + const expectedAngles = [Math.PI, Math.PI * 0.6, Math.PI * 0.4]; + expect(component.angles).toEqual(expectedAngles); + }); + + it("evenly divides angles when sum is 0", () => { + const data = [{ value: 0 }, { value: 0 }, { value: 0 }]; + const component = new DashboardPieChart({ + size: 100, + data, + colors: ["#FFF", "#000", "#AAA"], + }); + component.canvas = document.createElement("canvas"); + component.drawChart(); + const expectedAngles = Array(3).fill(Math.PI * (1 / 3) * 2); // Three items, so each gets 1/3 of the circle. + expect(component.angles).toEqual(expectedAngles); + }); + + it("returns null from detectHit when canvas context is not available", () => { + const instance = new DashboardPieChart({ size: 100, data: [], colors: [] }); + instance.canvas = document.createElement("canvas"); + instance.canvas.getContext = () => null; + const result = instance.detectHit(50, 50); + expect(result).toBeNull(); + }); + + it("returns from drawChart when canvas context is not available", () => { + const beginPatchSpy = jest.spyOn( + CanvasRenderingContext2D.prototype, + "beginPath" + ); + const instance = new DashboardPieChart({ size: 100, data: [], colors: [] }); + instance.canvas = document.createElement("canvas"); + instance.canvas.getContext = () => null; + instance.drawChart(); + expect(beginPatchSpy).not.toHaveBeenCalled(); + }); + + it("checks the hole in detectHit if holeStyle is provided, returning null if point is in path", () => { + jest + .spyOn(CanvasRenderingContext2D.prototype, "isPointInPath") + .mockReturnValue(true); + const instance = new DashboardPieChart({ + size: 100, + data: [], + colors: [], + holeStyle: { radius: 10, color: "#fff" }, + }); + instance.canvas = document.createElement("canvas"); + const result = instance.detectHit(50, 50); + expect(result).toBeNull(); + }); + + it("calls customRef when provided", () => { + const customRefMock = jest.fn(); + render( + + ); + expect(customRefMock).toHaveBeenCalledTimes(1); + expect(customRefMock.mock.calls[0][0]).toBeInstanceOf(HTMLElement); + }); +}); diff --git a/src/components/modules/DashboardModule/DashboardPieChart/DashboardPieChart.tsx b/src/components/modules/DashboardModule/DashboardPieChart/DashboardPieChart.tsx index 1ba7ff50..71ab8ae2 100644 --- a/src/components/modules/DashboardModule/DashboardPieChart/DashboardPieChart.tsx +++ b/src/components/modules/DashboardModule/DashboardPieChart/DashboardPieChart.tsx @@ -58,7 +58,7 @@ type Props = { @observer class DashboardPieChart extends React.Component { - canvas: HTMLCanvasElement | null | undefined; + canvas: HTMLCanvasElement | null = null; angles: number[] = []; diff --git a/src/components/modules/DashboardModule/DashboardTopEndpoints/DashboardTopEndpoints.spec.tsx b/src/components/modules/DashboardModule/DashboardTopEndpoints/DashboardTopEndpoints.spec.tsx new file mode 100644 index 00000000..d12c8032 --- /dev/null +++ b/src/components/modules/DashboardModule/DashboardTopEndpoints/DashboardTopEndpoints.spec.tsx @@ -0,0 +1,227 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { Endpoint } from "@src/@types/Endpoint"; +import { MigrationItem, ReplicaItem } from "@src/@types/MainItem"; +import { fireEvent, render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import DashboardTopEndpoints from "./DashboardTopEndpoints"; + +jest.mock("react-router-dom", () => ({ Link: "a" })); + +type BuildType = T extends "replica" + ? ReplicaItem + : MigrationItem; + +const buildItem = ( + type: T, + origin_endpoint_id: string, + destination_endpoint_id: string +): BuildType => { + const item = { + id: "", + type, + name: "", + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + origin_endpoint_id, + destination_endpoint_id, + notes: "", + origin_minion_pool_id: null, + destination_minion_pool_id: null, + instances: [""], + info: {}, + destination_environment: {}, + source_environment: {}, + transfer_result: null, + last_execution_status: "", + user_id: "", + }; + return item as BuildType; +}; + +const buildEndpoint = (id: string): Endpoint => ({ + id, + name: `${id}-name`, + description: "", + type: "openstack", + created_at: new Date().toISOString(), + mapped_regions: [], + connection_info: {}, +}); + +const replicas: DashboardTopEndpoints["props"]["replicas"] = [ + buildItem("replica", "a", "b"), + buildItem("replica", "a", "b"), + buildItem("replica", "c", "d"), +]; + +const migrations: DashboardTopEndpoints["props"]["migrations"] = [ + buildItem("migration", "e", "f"), + buildItem("migration", "e", "f"), + buildItem("migration", "e", "f"), +]; +const endpoints: DashboardTopEndpoints["props"]["endpoints"] = [ + buildEndpoint("a"), + buildEndpoint("b"), + buildEndpoint("c"), + buildEndpoint("d"), + buildEndpoint("e"), + buildEndpoint("f"), +]; + +describe("DashboardTopEndpoints", () => { + const defaultProps: DashboardTopEndpoints["props"] = { + replicas, + migrations, + endpoints, + style: {}, + loading: false, + onNewClick: jest.fn(), + }; + + it("should display a loading state", () => { + render( + + ); + expect(TestUtils.select("StatusImage__Image")).toBeTruthy(); + }); + + it("should display no data message", () => { + render( + + ); + expect(TestUtils.select("DashboardTopEndpoints__NoItems")).toBeTruthy(); + }); + + it("should trigger onNewClick when New Endpoint button is clicked", () => { + const onNewClickMock = jest.fn(); + render( + + ); + + fireEvent.click( + TestUtils.select("DashboardTopEndpoints__NoItems")?.querySelector( + "button" + )! + ); + expect(onNewClickMock).toHaveBeenCalledTimes(1); + }); + + it("should display the chart with data", () => { + render(); + + expect( + TestUtils.select("DashboardTopEndpoints__ChartWrapper") + ).toBeTruthy(); + expect( + TestUtils.selectAll( + "DashboardTopEndpoints__LegendLabel-" + )[0].attributes.getNamedItem("to")?.value + ).toBe("/endpoints/e"); + + expect( + TestUtils.selectAll("DashboardTopEndpoints__LegendLabel-")[1].textContent + ).toBe("f-name"); + }); + + it("should call calculateGroupedEndpoints when component receives new props", () => { + const calculateGroupedEndpointsSpy = jest.spyOn( + DashboardTopEndpoints.prototype, + "calculateGroupedEndpoints" + ); + + const { rerender } = render(); + + const newProps = { + ...defaultProps, + replicas: [], + migrations: [], + endpoints: [], + }; + rerender(); + + expect(calculateGroupedEndpointsSpy).toHaveBeenCalledWith(newProps); + expect(calculateGroupedEndpointsSpy).toHaveBeenCalledTimes(2); + }); + + it("should handle mouse over and update state", () => { + const setStateSpy = jest + .spyOn(DashboardTopEndpoints.prototype, "setState") + .mockImplementationOnce(() => {}); + + const instance = new DashboardTopEndpoints(defaultProps); + instance.chartRef = document.createElement("div"); + const groupedEndpoint = { + endpoint: endpoints[0], + replicasCount: 1, + migrationsCount: 2, + value: 3, + }; + instance.handleMouseOver(groupedEndpoint, 10, 10); + expect(setStateSpy).toHaveBeenCalledWith({ + groupedEndpoint, + tooltipPosition: { x: 26, y: -22 }, + }); + }); + + it("should handle mouse over and not update state if there's no chartRef", () => { + const setStateSpy = jest + .spyOn(DashboardTopEndpoints.prototype, "setState") + .mockImplementationOnce(() => {}); + + const instance = new DashboardTopEndpoints(defaultProps); + instance.chartRef = null; + const groupedEndpoint = { + endpoint: endpoints[0], + replicasCount: 1, + migrationsCount: 2, + value: 3, + }; + instance.handleMouseOver(groupedEndpoint, 10, 10); + expect(setStateSpy).not.toHaveBeenCalled(); + }); + + it("should handle mouse leave and update state", () => { + const setStateSpy = jest + .spyOn(DashboardTopEndpoints.prototype, "setState") + .mockImplementationOnce(() => {}); + + const instance = new DashboardTopEndpoints(defaultProps); + instance.handleMouseLeave(); + expect(setStateSpy).toHaveBeenCalledWith({ + groupedEndpoint: null, + }); + }); +}); diff --git a/src/components/modules/DashboardModule/DashboardTopEndpoints/DashboardTopEndpoints.tsx b/src/components/modules/DashboardModule/DashboardTopEndpoints/DashboardTopEndpoints.tsx index 81e9e0f5..2d1d20b3 100644 --- a/src/components/modules/DashboardModule/DashboardTopEndpoints/DashboardTopEndpoints.tsx +++ b/src/components/modules/DashboardModule/DashboardTopEndpoints/DashboardTopEndpoints.tsx @@ -141,7 +141,7 @@ type Props = { migrations: MigrationItem[]; // eslint-disable-next-line react/no-unused-prop-types endpoints: Endpoint[]; - style: any; + style: React.CSSProperties; loading: boolean; onNewClick: () => void; }; diff --git a/src/components/modules/DetailsModule/DetailsContentHeader/DetailsContentHeader.spec.tsx b/src/components/modules/DetailsModule/DetailsContentHeader/DetailsContentHeader.spec.tsx new file mode 100644 index 00000000..09b84c3c --- /dev/null +++ b/src/components/modules/DetailsModule/DetailsContentHeader/DetailsContentHeader.spec.tsx @@ -0,0 +1,122 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import TestUtils from "@tests/TestUtils"; + +import DetailsContentHeader from "./DetailsContentHeader"; + +jest.mock("react-router-dom", () => ({ Link: "a" })); + +describe("DetailsContentHeader", () => { + let defaultProps: DetailsContentHeader["props"]; + + beforeEach(() => { + defaultProps = { + dropdownActions: [{ label: "Test Action", action: () => {} }], + backLink: "/list", + typeImage: "type-image.jpeg", + primaryInfoPill: true, + statusPill: "COMPLETED", + statusLabel: "status-label", + itemTitle: "item-title", + itemType: "item-type", + itemDescription: "item-description", + }; + }); + + it("renders back button correctly", () => { + render(); + expect( + TestUtils.select( + "DetailsContentHeader__BackButton" + )?.attributes.getNamedItem("to")?.value + ).toBe("/list"); + }); + + it("renders type image when prop is provided", () => { + render(); + expect( + window.getComputedStyle( + TestUtils.select("DetailsContentHeader__TypeImage")! + ).background + ).toBe(`url(${defaultProps.typeImage}) no-repeat center`); + }); + + it("renders item title correctly", () => { + render(); + expect(TestUtils.select("DetailsContentHeader__Text")?.textContent).toBe( + defaultProps.itemTitle + ); + }); + + it("does not render status pill when statusPill prop is not provided", () => { + const newProps = { + ...defaultProps, + statusPill: undefined, + }; + render(); + expect(TestUtils.select("StatusPill__Wrapper")).toBeNull(); + }); + + it("renders status pill when statusPill prop is provided", () => { + render(); + expect(TestUtils.selectAll("StatusPill__Wrapper")).toHaveLength(2); + expect(TestUtils.selectAll("StatusPill__Wrapper")[0].textContent).toBe( + defaultProps.itemType + ); + expect(TestUtils.selectAll("StatusPill__Wrapper")[1].textContent).toBe( + defaultProps.statusLabel + ); + }); + + it("renders mock button when dropdownActions is not provided", () => { + const newProps = { + ...defaultProps, + dropdownActions: undefined, + }; + render(); + expect(TestUtils.select("DetailsContentHeader__MockButton")).toBeTruthy(); + }); + + it("renders ActionDropdown when dropdownActions is provided", () => { + render(); + expect(TestUtils.select("DetailsContentHeader__MockButton")).toBeNull(); + expect(TestUtils.select("DropdownButton__Wrapper")).toBeTruthy(); + userEvent.click(TestUtils.select("DropdownButton__Wrapper")!); + expect(TestUtils.selectAll("ActionDropdown__ListItem")[0].textContent).toBe( + "Test Action" + ); + }); + + it("does not render description when itemDescription is not provided", () => { + const newProps = { + ...defaultProps, + itemDescription: undefined, + }; + render(); + expect(TestUtils.select("DetailsContentHeader__Description")).toBeNull(); + }); + + it("renders description when itemDescription is provided", () => { + render(); + expect(TestUtils.select("DetailsContentHeader__Description")).toBeTruthy(); + expect( + TestUtils.select("DetailsContentHeader__Description")?.textContent + ).toBe(defaultProps.itemDescription); + }); +}); diff --git a/src/components/modules/DetailsModule/DetailsContentHeader/test.tsx b/src/components/modules/DetailsModule/DetailsContentHeader/test.tsx deleted file mode 100644 index 57a4562c..00000000 --- a/src/components/modules/DetailsModule/DetailsContentHeader/test.tsx +++ /dev/null @@ -1,82 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import DetailsContentHeader from "."; - -const wrap = props => - new TW(shallow(), "dcHeader"); - -const item = { - origin_endpoint_id: "openstack", - destination_endpoint_id: "azure", - instances: ["The instance title"], - type: "item type", - executions: [{ status: "COMPLETED", created_at: new Date() }], -}; - -describe("DetailsContentHeader Component", () => { - it("renders title", () => { - const wrapper = wrap({ item }); - expect(wrapper.findText("title")).toBe(item.instances[0]); - }); - - it("renders with no action button", () => { - const wrapper = wrap({ item }); - expect(wrapper.find("actionButton").length).toBe(0); - expect(wrapper.find("cancelButton").length).toBe(0); - }); - - it("renders with action button, if there are dropdown actions", () => { - const wrapper = wrap({ item, dropdownActions: [] }); - expect(wrapper.find("actionButton").length).toBe(1); - }); - - // it('dispatches back button click', () => { - // let onBackButonClick = sinon.spy() - // let wrapper = wrap({ item, onBackButonClick }) - // wrapper.find('backButton').click() - // expect(onBackButonClick.called).toBe(true) - // }) - - it("renders correct INFO pill", () => { - let wrapper = wrap({ item, primaryInfoPill: true }); - expect(wrapper.find("infoPill").prop("primary")).toBe(true); - expect(wrapper.find("infoPill").prop("label")).toBe("ITEM TYPE"); - expect(wrapper.find("infoPill").prop("alert")).toBe(undefined); - - wrapper = wrap({ item, alertInfoPill: true }); - expect(wrapper.find("infoPill").prop("alert")).toBe(true); - }); - - it("renders correct STATUS pill", () => { - let wrapper = wrap({ item }); - expect(wrapper.findPartialId("statusPill-").prop("status")).toBe( - "COMPLETED" - ); - const newItem = { ...item, executions: [...item.executions] }; - newItem.executions.push({ status: "RUNNING", created_at: new Date() }); - wrapper = wrap({ item: newItem }); - expect(wrapper.findPartialId("statusPill-").prop("status")).toBe("RUNNING"); - }); - - it("renders item description", () => { - const wrapper = wrap({ - item: { ...item, description: "item description" }, - }); - expect(wrapper.findText("description")).toBe("item description"); - }); -}); diff --git a/src/components/modules/DetailsModule/DetailsPageHeader/DetailsPageHeader.spec.tsx b/src/components/modules/DetailsModule/DetailsPageHeader/DetailsPageHeader.spec.tsx new file mode 100644 index 00000000..f7131818 --- /dev/null +++ b/src/components/modules/DetailsModule/DetailsPageHeader/DetailsPageHeader.spec.tsx @@ -0,0 +1,151 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; +import { act } from "react-dom/test-utils"; + +import { User } from "@src/@types/User"; +import notificationStore from "@src/stores/NotificationStore"; +import { render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import DetailsPageHeader from "./DetailsPageHeader"; + +jest.mock("react-router-dom", () => ({ Link: "a" })); +jest.mock("@src/stores/NotificationStore", () => ({ + notificationItems: [], + loadData: jest.fn().mockResolvedValue([]), + saveSeen: jest.fn(), +})); + +const user: User = { + id: "1", + name: "Test User", + email: "email", + project: { id: "1", name: "Test Project" }, +}; + +describe("DetailsPageHeader", () => { + let defaultProps: DetailsPageHeader["props"]; + + beforeEach(() => { + defaultProps = { + user, + onUserItemClick: jest.fn(), + }; + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it("renders without crashing", () => { + render(); + }); + + it("starts polling on mount", async () => { + render(); + expect(notificationStore.loadData).toHaveBeenCalled(); + + await act(async () => { + await Promise.resolve(); + }); + act(() => { + jest.advanceTimersByTime(15000); + }); + expect(notificationStore.loadData).toHaveBeenCalledTimes(2); + + await act(async () => { + await Promise.resolve(); + }); + act(() => { + jest.advanceTimersByTime(15000); + }); + expect(notificationStore.loadData).toHaveBeenCalledTimes(3); + }); + + it("stops polling on unmount", async () => { + const { unmount } = render(); + unmount(); + + await act(async () => { + await Promise.resolve(); + }); + act(() => { + jest.advanceTimersByTime(15000); + }); + expect(notificationStore.loadData).toHaveBeenCalledTimes(1); + }); + + it("handles notification close by saving seen notifications", () => { + render(); + TestUtils.select("NotificationDropdown__Icon")?.click(); + expect(TestUtils.select("NotificationDropdown__List")).toBeTruthy(); + TestUtils.select("NotificationDropdown__Icon")?.click(); + expect(TestUtils.select("NotificationDropdown__List")).toBeFalsy(); + expect(notificationStore.saveSeen).toHaveBeenCalled(); + }); + + it("handles user item click for 'about'", () => { + render(); + TestUtils.select("UserDropdown__Icon")?.click(); + expect(TestUtils.select("UserDropdown__List")).toBeTruthy(); + expect(TestUtils.select("UserDropdown__Username")?.textContent).toBe( + user.name + ); + expect(TestUtils.select("UserDropdown__Email")?.textContent).toBe( + user.email + ); + TestUtils.selectAll("UserDropdown__Label").forEach(item => { + if (item.textContent === "About Coriolis") { + item.click(); + } + }); + expect(defaultProps.onUserItemClick).not.toHaveBeenCalled(); + expect(TestUtils.select("AboutModal__Wrapper")).toBeTruthy(); + }); + + it("handles user item click for other values", () => { + render(); + TestUtils.select("UserDropdown__Icon")?.click(); + expect(TestUtils.select("UserDropdown__List")).toBeTruthy(); + TestUtils.selectAll("UserDropdown__Label").forEach(item => { + if (item.textContent === "Sign Out") { + item.click(); + } + }); + expect(defaultProps.onUserItemClick).toHaveBeenCalledWith({ + label: "Sign Out", + value: "signout", + }); + }); + + it("closes the about modal", () => { + render(); + TestUtils.select("UserDropdown__Icon")?.click(); + TestUtils.selectAll("UserDropdown__Label").forEach(item => { + if (item.textContent === "About Coriolis") { + item.click(); + } + }); + expect(TestUtils.select("AboutModal__Wrapper")).toBeTruthy(); + document.querySelectorAll("button").forEach(item => { + if (item.textContent === "Close") { + item.click(); + } + }); + expect(TestUtils.select("AboutModal__Wrapper")).toBeFalsy(); + }); +}); diff --git a/src/components/modules/DetailsModule/DetailsPageHeader/DetailsPageHeader.tsx b/src/components/modules/DetailsModule/DetailsPageHeader/DetailsPageHeader.tsx index 3e7567d1..59729d34 100644 --- a/src/components/modules/DetailsModule/DetailsPageHeader/DetailsPageHeader.tsx +++ b/src/components/modules/DetailsModule/DetailsPageHeader/DetailsPageHeader.tsx @@ -63,7 +63,6 @@ type Props = { label: React.ReactNode; value: string; }) => void; - testMode?: boolean; }; @observer @@ -77,9 +76,6 @@ class DetailsPageHeader extends React.Component { stopPolling!: boolean; UNSAFE_componentWillMount() { - if (this.props.testMode) { - return; - } this.stopPolling = false; this.pollData(true); } diff --git a/src/components/modules/DetailsModule/DetailsPageHeader/test.tsx b/src/components/modules/DetailsModule/DetailsPageHeader/test.tsx deleted file mode 100644 index 1074c0ff..00000000 --- a/src/components/modules/DetailsModule/DetailsPageHeader/test.tsx +++ /dev/null @@ -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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import type { User } from "@src/@types/User"; -import DetailsPageHeader from "."; - -type Props = { - user?: User | null; -}; - -const wrap = (props: Props) => - new TW( - shallow( - {}} testMode {...props} /> - ), - "dpHeader" - ); - -const user = { - name: "User name", - email: "email@email.com", - id: "user", - project: { id: "", name: "" }, -}; - -describe("DetailsPageHeader Component", () => { - it("renders with given user", () => { - const wrapper = wrap({ user }); - expect(wrapper.find("userDropdown").prop("user").name).toBe(user.name); - expect(wrapper.find("userDropdown").prop("user").email).toBe(user.email); - }); - - it("dispatches user item click", () => { - const onUserItemClick = sinon.spy(); - const wrapper = wrap({ user, onUserItemClick }); - wrapper - .find("userDropdown") - .simulate("itemClick", { value: "", label: "" }); - expect(onUserItemClick.called).toBe(true); - }); -}); diff --git a/src/components/modules/EndpointModule/ChooseProvider/ChooseProvider.spec.tsx b/src/components/modules/EndpointModule/ChooseProvider/ChooseProvider.spec.tsx new file mode 100644 index 00000000..35a339bf --- /dev/null +++ b/src/components/modules/EndpointModule/ChooseProvider/ChooseProvider.spec.tsx @@ -0,0 +1,383 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { Endpoint } from "@src/@types/Endpoint"; +import { ThemePalette } from "@src/components/Theme"; +import notificationStore from "@src/stores/NotificationStore"; +import FileUtils from "@src/utils/FileUtils"; +import { fireEvent, render, waitFor } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import ChooseProvider from "./ChooseProvider"; + +const OPENSTACK_ENDPOINT: Endpoint = { + name: "Openstack", + type: "openstack", + id: "1", + description: "", + created_at: new Date().toISOString(), + mapped_regions: ["region_1"], + connection_info: {}, +}; + +const mockDataTransfer = (files: any[]) => ({ + dataTransfer: { + dropEffect: "none", + files, + items: files.map(file => ({ + kind: "file", + type: file.type, + getAsFile: () => file, + })), + types: ["Files"], + }, +}); + +jest.mock("react-router-dom", () => ({ Link: "a" })); + +jest.mock("@src/stores/NotificationStore", () => ({ + alert: jest.fn(), +})); + +jest.mock("@src/utils/Config", () => ({ + config: { + providerSortPriority: {}, + providerNames: { + openstack: "OpenStack", + vmware_vsphere: "VMware vSphere", + }, + }, +})); + +describe("ChooseProvider", () => { + let defaultProps: ChooseProvider["props"]; + let readContentFromFileListMock: jest.SpyInstance; + + beforeEach(() => { + jest.useFakeTimers(); + + readContentFromFileListMock = jest.spyOn( + FileUtils, + "readContentFromFileList" + ); + readContentFromFileListMock.mockResolvedValue([ + { + name: "openstack.endpoint", + content: JSON.stringify(OPENSTACK_ENDPOINT), + }, + ]); + + defaultProps = { + providers: ["openstack", "vmware_vsphere"], + regions: [ + { + id: "region_1", + name: "Region 1", + description: "", + enabled: true, + mapped_endpoints: [], + }, + ], + onCancelClick: jest.fn(), + onProviderClick: jest.fn(), + onUploadEndpoint: jest.fn(), + loading: false, + onValidateMultipleEndpoints: jest.fn(), + multiValidating: false, + multiValidation: [], + onRemoveEndpoint: jest.fn(), + onResetValidation: jest.fn(), + }; + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it("renders without crashing", () => { + render(); + expect(TestUtils.select("Button__")?.textContent).toBe("Cancel"); + }); + + it('calls "onProviderClick" when a provider logo is clicked', () => { + render(); + + const providerButton = TestUtils.select("EndpointLogos__Wrapper")!; + fireEvent.click(providerButton); + expect(defaultProps.onProviderClick).toHaveBeenCalledWith("openstack"); + }); + + it("shows loading state when loading is true", () => { + render(); + expect(TestUtils.select("ChooseProvider__LoadingWrapper")).toBeTruthy(); + }); + + it("handles file uploads and parses single files", async () => { + const onUploadEndpointMock = jest.fn(); + render( + + ); + const fileInput = document.querySelector('input[type="file"]')!; + + const file = new File( + [JSON.stringify(OPENSTACK_ENDPOINT)], + "openstack.endpoint", + { + type: "application/json", + } + ); + + expect(fileInput).toBeTruthy(); + expect(defaultProps.onUploadEndpoint).not.toHaveBeenCalled(); + + fireEvent.change(fileInput, { target: { files: [file] } }); + + await waitFor(() => expect(onUploadEndpointMock).toHaveBeenCalled()); + }); + + it("highlights dropzone on drag enter", () => { + render(); + jest.advanceTimersByTime(1000); + const uploadArea = TestUtils.select("ChooseProvider__Upload")!; + const style = () => window.getComputedStyle(uploadArea); + + expect(style().border).toBe(`1px dashed white`); + fireEvent.dragEnter(window, mockDataTransfer([])); + expect(style().border).toBe( + `1px dashed ${ThemePalette.primary.toLowerCase()}` + ); + }); + + it("removes highlight from dropzone on drag leave", () => { + render(); + jest.advanceTimersByTime(1000); + const uploadArea = TestUtils.select("ChooseProvider__Upload")!; + const style = () => window.getComputedStyle(uploadArea); + + fireEvent.dragLeave(window, mockDataTransfer([])); + expect(style().border).toBe(`1px dashed white`); + }); + + it("processes file on drop", async () => { + render(); + jest.advanceTimersByTime(1000); + const file = new File(["endpoint content"], "openstack.endpoint", { + type: "application/json", + }); + fireEvent.drop(window, mockDataTransfer([file])); + + await waitFor(() => + expect(FileUtils.readContentFromFileList).toHaveBeenCalled() + ); + }); + + it("displays error notification for invalid file content", async () => { + readContentFromFileListMock.mockResolvedValue([ + { + name: "invalid.endpoint", + content: "invalid content", + }, + ]); + render(); + jest.advanceTimersByTime(1000); + const file = new File(["invalid content"], "invalid.endpoint", { + type: "application/json", + }); + fireEvent.drop(window, mockDataTransfer([file])); + + await waitFor(() => + expect(notificationStore.alert).toHaveBeenCalledWith( + "Invalid .endpoint file", + "error" + ) + ); + }); + + it("displays error notification if endpoint has no name", async () => { + readContentFromFileListMock.mockResolvedValue([ + { + name: "invalid.endpoint", + content: JSON.stringify({ + OPENSTACK_ENDPOINT, + name: "", + }), + }, + ]); + render(); + jest.advanceTimersByTime(1000); + const file = new File(["invalid content"], "invalid.endpoint", { + type: "application/json", + }); + fireEvent.drop(window, mockDataTransfer([file])); + + await waitFor(() => + expect(notificationStore.alert).toHaveBeenCalledWith( + "Invalid .endpoint file", + "error" + ) + ); + }); + + it("processes multiple files and handles unique names", async () => { + const multipleFilesMeta = [ + { + name: "file1.endpoint", + content: JSON.stringify(OPENSTACK_ENDPOINT), + }, + { + name: "file2.endpoint", + content: JSON.stringify(OPENSTACK_ENDPOINT), + }, + ]; + const multipleFiles = multipleFilesMeta.map( + ({ name, content }) => + new File([content], name, { + type: "application/json", + }) + ); + + readContentFromFileListMock.mockResolvedValue(multipleFilesMeta); + + let setStateObj: any = {}; + jest + .spyOn(ChooseProvider.prototype, "setState") + .mockImplementationOnce((obj: any) => { + setStateObj = obj; + }); + + render(); + const fileInput = document.querySelector('input[type="file"]')!; + fireEvent.change(fileInput, { target: { files: multipleFiles } }); + + await waitFor(() => { + expect(defaultProps.onResetValidation).toHaveBeenCalledTimes(1); + expect(setStateObj.multipleUploadedEndpoints[0].name).toBe( + OPENSTACK_ENDPOINT.name + ); + expect(setStateObj.multipleUploadedEndpoints[1].name).toBe( + `${OPENSTACK_ENDPOINT.name} (1)` + ); + }); + }); + + it("fires onresizeupdate when multipleUploadedEndpoints changes", async () => { + const onResizeUpdateMock = jest.fn(); + const multipleFilesMeta = [ + { + name: "file1.endpoint", + content: JSON.stringify(OPENSTACK_ENDPOINT), + }, + { + name: "file2.endpoint", + content: JSON.stringify(OPENSTACK_ENDPOINT), + }, + ]; + const multipleFiles = multipleFilesMeta.map( + ({ name, content }) => + new File([content], name, { + type: "application/json", + }) + ); + readContentFromFileListMock.mockResolvedValue(multipleFilesMeta); + + render( + + ); + const fileInput = document.querySelector('input[type="file"]')!; + fireEvent.change(fileInput, { target: { files: multipleFiles } }); + + await waitFor(() => { + expect(onResizeUpdateMock).toHaveBeenCalled(); + }); + }); + + it("adds drop effect on drag over", async () => { + render(); + jest.advanceTimersByTime(1000); + + const transfer = mockDataTransfer([]); + fireEvent.dragOver(window, transfer); + + await waitFor(() => { + expect(transfer.dataTransfer.dropEffect).toBe("copy"); + }); + }); + + it("shows warning for unindetified regions", async () => { + readContentFromFileListMock.mockResolvedValue([ + { + name: "openstack.endpoint", + content: JSON.stringify({ + ...OPENSTACK_ENDPOINT, + mapped_regions: ["region_2"], + }), + }, + ]); + render(); + jest.advanceTimersByTime(1000); + const file = new File(["invalid content"], "openstack.endpoint", { + type: "application/json", + }); + fireEvent.drop(window, mockDataTransfer([file])); + + await waitFor(() => + expect(notificationStore.alert).toHaveBeenCalledWith( + "1 Coriolis Region couldn't be mapped", + "warning" + ) + ); + }); + + it("processes remove endpoint", () => { + const chooseProviderInstance = new ChooseProvider(defaultProps); + jest + .spyOn(chooseProviderInstance, "setState") + .mockImplementation((callback: any) => { + callback({ multipleUploadedEndpoints: [OPENSTACK_ENDPOINT] }); + }); + + chooseProviderInstance.handleRemoveUploadedEndpoint( + OPENSTACK_ENDPOINT, + true + ); + + expect(defaultProps.onRemoveEndpoint).toHaveBeenCalledWith( + OPENSTACK_ENDPOINT + ); + }); + + it("handles regions change", () => { + const chooseProviderInstance = new ChooseProvider(defaultProps); + let nextState: any = {}; + jest + .spyOn(chooseProviderInstance, "setState") + .mockImplementation((callback: any) => { + nextState = callback({ + multipleUploadedEndpoints: [OPENSTACK_ENDPOINT], + }); + }); + + chooseProviderInstance.handleRegionsChange(OPENSTACK_ENDPOINT, [ + "region_2", + ]); + expect(nextState.multipleUploadedEndpoints[0].mapped_regions).toEqual([ + "region_2", + ]); + }); +}); diff --git a/src/components/modules/EndpointModule/ChooseProvider/MultipleUploadedEndpoints.spec.tsx b/src/components/modules/EndpointModule/ChooseProvider/MultipleUploadedEndpoints.spec.tsx new file mode 100644 index 00000000..089dca5d --- /dev/null +++ b/src/components/modules/EndpointModule/ChooseProvider/MultipleUploadedEndpoints.spec.tsx @@ -0,0 +1,288 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { Endpoint } from "@src/@types/Endpoint"; +import EndpointLogos from "@src/components/modules/EndpointModule/EndpointLogos"; +import DropdownLink from "@src/components/ui/Dropdowns/DropdownLink"; +import DomUtils from "@src/utils/DomUtils"; +import { fireEvent, render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import MultipleUploadedEndpoints from "./MultipleUploadedEndpoints"; + +jest.mock("@src/components/modules/EndpointModule/EndpointLogos", () => ({ + __esModule: true, + default: (props: EndpointLogos["props"]) =>
{props.endpoint}
, +})); + +jest.mock("@src/components/ui/Dropdowns/DropdownLink", () => ({ + __esModule: true, + default: (props: DropdownLink["props"]) => ( +
+ {props.items.map(item => ( +
{ + props.onChange && props.onChange(item); + }} + key={item.value} + > + {item.label} - {item.value} + {props.getLabel && props.getLabel()} +
+ ))} +
+ ), +})); + +jest.mock("@src/utils/DomUtils", () => ({ + copyTextToClipboard: jest.fn(), +})); + +const OPENSTACK_ENDPOINT: Endpoint = { + name: "Openstack", + type: "openstack", + id: "1", + description: "", + created_at: new Date().toISOString(), + mapped_regions: [], + connection_info: {}, +}; + +const AWS_ENDPOINT: Endpoint = { + name: "AWS", + type: "aws", + id: "2", + description: "", + created_at: new Date().toISOString(), + mapped_regions: [], + connection_info: {}, +}; + +describe("MultipleUploadedEndpoints", () => { + let defaultProps: MultipleUploadedEndpoints["props"]; + let copyTextToClipboard: jest.SpyInstance; + + beforeEach(() => { + copyTextToClipboard = jest.spyOn(DomUtils, "copyTextToClipboard"); + + defaultProps = { + endpoints: [OPENSTACK_ENDPOINT, AWS_ENDPOINT], + regions: [ + { + id: "default", + name: "Default", + description: "", + enabled: true, + mapped_endpoints: [], + }, + ], + invalidRegionsEndpointIds: [], + multiValidation: [ + { + endpoint: OPENSTACK_ENDPOINT, + validating: false, + validation: { valid: true, message: "" }, + }, + { + endpoint: AWS_ENDPOINT, + validating: false, + validation: { valid: false, message: "Invalid" }, + }, + ], + validating: false, + onRegionsChange: jest.fn(), + onBackClick: jest.fn(), + onRemove: jest.fn(), + onValidateClick: jest.fn(), + onDone: jest.fn(), + }; + }); + + it("renders without crashing", () => { + render(); + + expect( + TestUtils.selectAll("MultipleUploadedEndpoints__EndpointName")[0] + .textContent + ).toBe("Openstack"); + expect( + TestUtils.selectAll("MultipleUploadedEndpoints__EndpointName")[1] + .textContent + ).toBe("AWS"); + }); + + it('handles the "Back" button click', () => { + render(); + document.querySelectorAll("button").forEach(button => { + if (button.textContent === "Back") { + fireEvent.click(button); + } + }); + expect(defaultProps.onBackClick).toHaveBeenCalled(); + }); + + it('handles the "Validate and Save" button click', () => { + render(); + document.querySelectorAll("button").forEach(button => { + if (button.textContent === "Validate and save") { + fireEvent.click(button); + } + }); + expect(defaultProps.onValidateClick).toHaveBeenCalled(); + }); + + it('changes to "Done" button after validation', async () => { + const { rerender, getByText } = render( + + ); + expect(TestUtils.select("LoadingButton__Loading")).toBeTruthy(); + rerender( + + ); + expect(TestUtils.select("LoadingButton__Loading")).toBeFalsy(); + expect(getByText("Done")).toBeTruthy(); + }); + + it('handles the "Done" button click', () => { + const { rerender, getByText } = render( + + ); + rerender( + + ); + fireEvent.click(getByText("Done")); + expect(defaultProps.onDone).toHaveBeenCalled(); + }); + + it("removes an endpoint", () => { + render(); + const deleteButtons = TestUtils.selectAll( + "MultipleUploadedEndpoints__DeleteButton" + ); + fireEvent.click(deleteButtons[0]); + expect(defaultProps.onRemove).toHaveBeenCalledWith( + defaultProps.endpoints[0], + true + ); + }); + + it("copies an error message to the clipboard", () => { + copyTextToClipboard.mockImplementation(() => Promise.resolve(true)); + render(); + fireEvent.click(TestUtils.selectAll("StatusIcon__Wrapper")[1]); + expect(DomUtils.copyTextToClipboard).toHaveBeenCalled(); + }); + + it("removes an uploaded non multi endpoint", () => { + const newProps = { + ...defaultProps, + multiValidation: [], + }; + + render(); + const deleteButtons = TestUtils.selectAll( + "MultipleUploadedEndpoints__DeleteButton" + ); + fireEvent.click(deleteButtons[0]); + expect(defaultProps.onRemove).toHaveBeenCalledWith( + defaultProps.endpoints[0], + false + ); + }); + + it("selects valid region when there's invalid regions", () => { + const newProps = { + ...defaultProps, + multiValidation: [], + invalidRegionsEndpointIds: [ + { id: "openstackOpenstack", regions: ["default"] }, + ], + }; + + render(); + fireEvent.click( + document.querySelector("[data-testid='DropdownLink__Item']")! + ); + expect(defaultProps.onRegionsChange).toHaveBeenCalledWith( + OPENSTACK_ENDPOINT, + ["default"] + ); + }); + + it("selects valid region when there's invalid regions with endpoints mapped regions", () => { + const newOpenstackEndpoint = { + ...OPENSTACK_ENDPOINT, + mapped_regions: ["default"], + }; + const newProps = { + ...defaultProps, + endpoints: [newOpenstackEndpoint], + multiValidation: [], + invalidRegionsEndpointIds: [ + { id: "openstackOpenstack", regions: ["default"] }, + ], + }; + + render(); + fireEvent.click( + document.querySelector("[data-testid='DropdownLink__Item']")! + ); + expect(defaultProps.onRegionsChange).toHaveBeenCalledWith( + newOpenstackEndpoint, + [] + ); + }); + + it("shows validating status when validating an endpoint", () => { + const newProps = { + ...defaultProps, + multiValidation: [ + { + endpoint: OPENSTACK_ENDPOINT, + validating: true, + validation: { valid: true, message: "" }, + }, + ], + }; + + render(); + expect(TestUtils.selectAll("StatusIcon__Wrapper")).toHaveLength(1); + }); + + it("shows no status if no validation", () => { + const newProps = { + ...defaultProps, + multiValidation: [{ endpoint: OPENSTACK_ENDPOINT, validating: false }], + }; + + render(); + expect(TestUtils.selectAll("StatusIcon__Wrapper")).toHaveLength(0); + }); + + it("shows invalid endpoint for unsupported endpoint type", () => { + const newProps = { + ...defaultProps, + endpoints: ["invalid"], + }; + + render(); + expect( + TestUtils.select("MultipleUploadedEndpoints__InvalidEndpoint") + ?.textContent + ).toContain("unsupported provider type: invalid"); + }); +}); diff --git a/src/components/modules/EndpointModule/ChooseProvider/MultipleUploadedEndpoints.tsx b/src/components/modules/EndpointModule/ChooseProvider/MultipleUploadedEndpoints.tsx index 91974ad8..931d77c2 100644 --- a/src/components/modules/EndpointModule/ChooseProvider/MultipleUploadedEndpoints.tsx +++ b/src/components/modules/EndpointModule/ChooseProvider/MultipleUploadedEndpoints.tsx @@ -113,7 +113,7 @@ class MultipleUploadedEndpoints extends React.Component { validationDone: false, }; - UNSAFE_componentWillReceiveProps(prevProps: Props) { + componentDidUpdate(prevProps: Props) { if (prevProps.validating && !this.props.validating) { this.setState({ validationDone: true }); } diff --git a/src/components/modules/EndpointModule/ChooseProvider/test.tsx b/src/components/modules/EndpointModule/ChooseProvider/test.tsx deleted file mode 100644 index c3968d01..00000000 --- a/src/components/modules/EndpointModule/ChooseProvider/test.tsx +++ /dev/null @@ -1,55 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import ChooseProvider from "."; - -const wrap = props => - new TW(shallow(), "cProvider"); - -const providers = [ - "azure", - "openstack", - "opc", - "oracle_vm", - "vmware_vsphere", - "aws", -]; - -describe("ChooseProvider Component", () => { - it("renders all given providers", () => { - const wrapper = wrap({ providers }); - providers.forEach(key => { - expect(wrapper.find(`endpointLogo-${key}`).prop("endpoint")).toBe(key); - }); - }); - - it("dispatches provider click", () => { - const onProviderClick = sinon.spy(); - const wrapper = wrap({ providers, onProviderClick }); - wrapper.find("endpointLogo-opc").click(); - expect(onProviderClick.calledOnce).toBe(true); - expect(onProviderClick.args[0][0]).toBe("opc"); - }); - - it("dispatches cancel click", () => { - const onCancelClick = sinon.spy(); - const wrapper = wrap({ providers, onCancelClick }); - wrapper.find("cancelButton").click(); - expect(onCancelClick.calledOnce).toBe(true); - }); -}); diff --git a/src/components/modules/EndpointModule/EndpointDetailsContent/EndpointDetailsContent.spec.tsx b/src/components/modules/EndpointModule/EndpointDetailsContent/EndpointDetailsContent.spec.tsx new file mode 100644 index 00000000..19caf45f --- /dev/null +++ b/src/components/modules/EndpointModule/EndpointDetailsContent/EndpointDetailsContent.spec.tsx @@ -0,0 +1,334 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { Endpoint } from "@src/@types/Endpoint"; +import DomUtils from "@src/utils/DomUtils"; +import { fireEvent, render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import EndpointDetailsContent from "./EndpointDetailsContent"; + +jest.mock("@src/utils/Config", () => ({ + config: { + providerSortPriority: {}, + providerNames: { + openstack: "OpenStack", + vmware_vsphere: "VMware vSphere", + }, + passwordFields: ["secret_key"], + }, +})); + +jest.mock("react-router-dom", () => ({ + Link: "div", +})); + +const OPENSTACK_ENDPOINT: Endpoint = { + name: "Openstack", + type: "openstack", + id: "1", + description: "openstack description", + created_at: new Date().toISOString(), + mapped_regions: [], + connection_info: {}, +}; + +const USAGE = { + migrations: [ + { + type: "migration", + id: "mig-1", + instances: [], + notes: "Migration 1", + }, + { + type: "migration", + id: "mig-2", + instances: ["mig-vm1"], + }, + ], + replicas: [ + { + type: "replica", + id: "rep-1", + instances: [], + notes: "Replica 1", + }, + ], +}; + +describe("EndpointDetailsContent", () => { + let defaultProps: EndpointDetailsContent["props"]; + let domDownload: jest.SpyInstance; + + beforeEach(() => { + domDownload = jest.spyOn(DomUtils, "download"); + + defaultProps = { + item: OPENSTACK_ENDPOINT, + regions: [ + { + id: "1", + name: "Region 1", + description: "region description", + enabled: true, + mapped_endpoints: [], + }, + ], + connectionInfo: null, + usage: USAGE as any, + loading: false, + connectionInfoSchema: [], + onDeleteClick: jest.fn(), + onValidateClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("openstack description")).toBeTruthy(); + }); + + it("renders loading state correctly", () => { + render(); + expect( + TestUtils.select("EndpointDetailsContent__LoadingWrapper") + ).toBeTruthy(); + }); + + it("handles the delete button click", () => { + render(); + document.querySelectorAll("button").forEach(button => { + if (button.textContent === "Delete Endpoint") { + fireEvent.click(button); + } + }); + expect(defaultProps.onDeleteClick).toBeCalled(); + }); + + it("downloads file field", () => { + const { getByText } = render( + + ); + fireEvent.click(getByText("Download")); + + expect(domDownload).toBeCalledWith("file content", "file"); + }); + + it("fails to download if no endpoint", () => { + const instance = new EndpointDetailsContent({ + ...defaultProps, + item: null, + }); + const result = instance.renderDownloadValue("file", "content"); + expect(result).toBe(null); + }); + + it("doesn't render secret_ref", () => { + render( + + ); + let secretRef; + TestUtils.selectAll("CopyValue__Value").forEach(element => { + if (element.textContent === "secret_ref") { + secretRef = element; + } + }); + expect(secretRef).toBe(undefined); + }); + + it("renders objects in connection info", () => { + const { getByText } = render( + + ); + expect(getByText("Nested Prop")).toBeTruthy(); + expect(getByText("nested prop's value")).toBeTruthy(); + }); + + it("doesn't render the same key twice", () => { + const connInfo = { + field1: "value1", + }; + const instance = new EndpointDetailsContent({ + ...defaultProps, + connectionInfoSchema: [ + { + name: "field1", + type: "string", + }, + ], + }); + instance.renderedKeys = {}; + + let result: any = instance.renderConnectionInfo(connInfo); + expect(result[0]).not.toBeNull(); + + result = instance.renderConnectionInfo(connInfo); + expect(result[0]).toBeNull(); + }); + + it("renders booleans correctly", () => { + const { getByText, rerender } = render( + + ); + expect(getByText("Bool Field")).toBeTruthy(); + expect(getByText("Yes")).toBeTruthy(); + + rerender( + + ); + expect(getByText("Bool Field")).toBeTruthy(); + expect(getByText("No")).toBeTruthy(); + + rerender( + + ); + let boolValue; + TestUtils.selectAll("CopyValue__Value").forEach(element => { + if (element.textContent === "-") { + boolValue = element; + } + }); + expect(boolValue).toBeTruthy(); + expect(getByText("Bool Field")).toBeTruthy(); + }); + + it("renders password fields correctly", () => { + const { getByText } = render( + + ); + expect(getByText("Password Field")).toBeTruthy(); + expect(getByText("•••••••••")).toBeTruthy(); + }); + + it("renders passwords from config correctly", () => { + const { getByText } = render( + + ); + expect(getByText("Secret Key")).toBeTruthy(); + expect(getByText("•••••••••")).toBeTruthy(); + }); + + it("renders regions correctly", () => { + const { getByText } = render( + + ); + expect(getByText("Region 1")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/EndpointModule/EndpointDetailsContent/test.tsx b/src/components/modules/EndpointModule/EndpointDetailsContent/test.tsx deleted file mode 100644 index 7ec226a4..00000000 --- a/src/components/modules/EndpointModule/EndpointDetailsContent/test.tsx +++ /dev/null @@ -1,120 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import moment from "moment"; -import TW from "@src/utils/TestWrapper"; -import EndpointDetailsContent from "."; - -import configLoader from "@src/utils/Config"; - -const wrap = props => - new TW( - shallow( - - ), - "edContent" - ); - -const item = { - name: "endpoint_name", - type: "openstack", - description: "endpoint_description", - created_at: new Date(2017, 10, 24, 13, 56), -}; - -const connectionInfo = { - username: "username", - password: "password123", - details: "other details", - boolean_true: true, - boolean_false: false, - nested: { - nested_1: "nested_first", - nested_2: "nested_second", - }, -}; - -describe("EndpointDetailsContent Component", () => { - beforeAll(() => { - configLoader.config = { passwordFields: [] }; - }); - - it("renders endpoint details", () => { - const wrapper = wrap({ item }); - expect(wrapper.find("name").prop("value")).toBe(item.name); - expect(wrapper.find("description").prop("value")).toBe(item.description); - expect(wrapper.find("created").prop("value")).toBe( - moment(item.created_at) - .add(-new Date().getTimezoneOffset(), "minutes") - .format("DD/MM/YYYY HH:mm") - ); - expect(wrapper.find("connLoading").length).toBe(0); - }); - - it("renders connection info loading", () => { - const wrapper = wrap({ item, loading: true }); - expect(wrapper.find("name").prop("value")).toBe(item.name); - expect(wrapper.find("connLoading").length).toBe(1); - }); - - it("renders simple connection info", () => { - const wrapper = wrap({ - item, - connectionInfo, - passwordFields: ["password"], - }); - expect(wrapper.find("connValue-username").prop("value")).toBe( - connectionInfo.username - ); - expect(wrapper.find("connPassword").prop("value")).toBe( - connectionInfo.password - ); - expect(wrapper.find("connValue-details").prop("value")).toBe( - connectionInfo.details - ); - }); - - it("renders boolean as Yes and No", () => { - const wrapper = wrap({ item, connectionInfo }); - expect(wrapper.find("connValue-boolean_true").prop("value")).toBe("Yes"); - expect(wrapper.find("connValue-boolean_false").prop("value")).toBe("No"); - }); - - it("renders nested connection info", () => { - const wrapper = wrap({ item, connectionInfo }); - expect(wrapper.find("connValue-nested_1").prop("value")).toBe( - connectionInfo.nested.nested_1 - ); - expect(wrapper.find("connValue-nested_2").prop("value")).toBe( - connectionInfo.nested.nested_2 - ); - }); - - it("dispatches buttons clicks", () => { - const onDeleteClick = sinon.spy(); - const onValidateClick = sinon.spy(); - - const wrapper = wrap({ item, onDeleteClick, onValidateClick }); - wrapper.find("validateButton").click(); - wrapper.find("deleteButton").click(); - expect(onValidateClick.calledOnce).toBe(true); - expect(onDeleteClick.calledOnce).toBe(true); - }); -}); diff --git a/src/components/modules/EndpointModule/EndpointDuplicateOptions/EndpointDuplicateOptions.spec.tsx b/src/components/modules/EndpointModule/EndpointDuplicateOptions/EndpointDuplicateOptions.spec.tsx new file mode 100644 index 00000000..651c3023 --- /dev/null +++ b/src/components/modules/EndpointModule/EndpointDuplicateOptions/EndpointDuplicateOptions.spec.tsx @@ -0,0 +1,93 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { fireEvent, render } from "@testing-library/react"; + +import EndpointDuplicateOptions from "./EndpointDuplicateOptions"; + +jest.mock("@src/components/ui/FieldInput", () => ({ + __esModule: true, + default: (props: any) => ( +
{ + props.onChange && props.onChange("1"); + }} + > + {props.label} - {props.value} +
+ ), +})); + +describe("EndpointDuplicateOptions", () => { + let defaultProps: EndpointDuplicateOptions["props"]; + + beforeEach(() => { + defaultProps = { + projects: [ + { id: "1", name: "admin" }, + { id: "2", name: "admin2" }, + ], + selectedProjectId: "2", + duplicating: false, + onCancelClick: jest.fn(), + onDuplicateClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render( + + ); + expect(getByText("Duplicate To Project - 2")).toBeTruthy(); + }); + + it("handles submit on Enter key press", () => { + render(); + + fireEvent.keyDown(document, { key: "Enter" }); + + expect(defaultProps.onDuplicateClick).toHaveBeenCalledWith("2"); + }); + + it("shows duplicating status", () => { + const { getByText } = render( + + ); + + expect(getByText("Duplicating Endpoint")).toBeTruthy(); + }); + + it("changes project", () => { + const { getByTestId } = render( + + ); + + fireEvent.click(getByTestId("FieldInput__Wrapper")); + expect(getByTestId("FieldInput__Wrapper").textContent).toBe( + "Duplicate To Project - 1" + ); + }); + + it("handles duplicate click", () => { + const { getByText } = render( + + ); + + fireEvent.click(getByText("Duplicate")); + expect(defaultProps.onDuplicateClick).toHaveBeenCalledWith("2"); + }); +}); diff --git a/src/components/modules/EndpointModule/EndpointDuplicateOptions/test.tsx b/src/components/modules/EndpointModule/EndpointDuplicateOptions/test.tsx deleted file mode 100644 index 59883945..00000000 --- a/src/components/modules/EndpointModule/EndpointDuplicateOptions/test.tsx +++ /dev/null @@ -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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import type { Project } from "@src/@types/Project"; -import EndpointDuplicateOptions from "."; - -type Props = { - projects: Project[]; - selectedProjectId: string; - duplicating: boolean; - onCancelClick: () => void; - onDuplicateClick: (projectId: string) => void; -}; - -const wrap = (props: Props) => - new TW(shallow(), "edOptions"); -const projects: Project[] = [ - { id: "project-1", name: "Project 1" }, - { id: "project-2", name: "Project 2" }, -]; -describe("EndpointDuplicateOptions Component", () => { - it("renders projects", () => { - const wrapper = wrap({ - projects, - selectedProjectId: "project-2", - duplicating: false, - onCancelClick: () => {}, - onDuplicateClick: () => {}, - }); - expect(wrapper.find("field-project").prop("enum")[1].name).toBe( - projects[1].name - ); - expect(wrapper.find("field-project").prop("value")).toBe("project-2"); - expect(wrapper.find("loading").length).toBe(0); - }); - - it("dispatches duplicate", () => { - const onDuplicateClick = sinon.spy(); - const wrapper = wrap({ - projects, - selectedProjectId: "project-2", - duplicating: false, - onCancelClick: () => {}, - onDuplicateClick, - }); - wrapper.find("duplicateButton").click(); - expect(onDuplicateClick.args[0][0]).toBe("project-2"); - }); - - it("renders loading", () => { - const wrapper = wrap({ - projects, - selectedProjectId: "project-2", - duplicating: true, - onCancelClick: () => {}, - onDuplicateClick: () => {}, - }); - expect(wrapper.find("loading").length).toBe(1); - }); -}); diff --git a/src/components/modules/EndpointModule/EndpointListItem/EndpointListItem.spec.tsx b/src/components/modules/EndpointModule/EndpointListItem/EndpointListItem.spec.tsx new file mode 100644 index 00000000..e01a2327 --- /dev/null +++ b/src/components/modules/EndpointModule/EndpointListItem/EndpointListItem.spec.tsx @@ -0,0 +1,91 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { Endpoint } from "@src/@types/Endpoint"; +import { render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import EndpointListItem from "./EndpointListItem"; + +jest.mock("@src/components/modules/EndpointModule/EndpointLogos", () => ({ + __esModule: true, + default: (props: any) => ( +
{props.endpoint}
+ ), +})); + +const OPENSTACK_ENDPOINT: Endpoint = { + name: "Openstack", + type: "openstack", + id: "1", + description: "openstack description", + created_at: new Date().toISOString(), + mapped_regions: [], + connection_info: {}, +}; + +describe("EndpointListItem", () => { + let defaultProps: EndpointListItem["props"]; + + beforeEach(() => { + defaultProps = { + item: OPENSTACK_ENDPOINT, + onClick: jest.fn(), + selected: false, + onSelectedChange: jest.fn(), + getUsage: jest.fn().mockImplementation(() => ({ + replicasCount: 3, + migrationsCount: 2, + })), + }; + }); + + it("renders without crashing", () => { + const { getByText, getByTestId } = render( + + ); + expect(getByText(OPENSTACK_ENDPOINT.name)).toBeTruthy(); + expect(getByText(OPENSTACK_ENDPOINT.description)).toBeTruthy(); + expect(getByTestId("EndpointLogos").textContent).toBe( + OPENSTACK_ENDPOINT.type + ); + }); + + it("renders usage", () => { + const { getByText } = render(); + expect(defaultProps.getUsage).toHaveBeenCalledWith(OPENSTACK_ENDPOINT); + expect(getByText("2 migrations, 3 replicas")).toBeTruthy(); + }); + + it("renders N/A when no description", () => { + const newProps = { + ...defaultProps, + item: { + ...OPENSTACK_ENDPOINT, + description: "", + }, + }; + const { getByText } = render(); + expect(getByText("N/A")).toBeTruthy(); + }); + + it("renders selected checkbox", () => { + render(); + const checkbox = TestUtils.selectContains("EndpointListItem__Checkbox")!; + const style = window.getComputedStyle(checkbox); + expect(style.opacity).toBe("1"); + }); +}); diff --git a/src/components/modules/EndpointModule/EndpointListItem/test.tsx b/src/components/modules/EndpointModule/EndpointListItem/test.tsx deleted file mode 100644 index d572bdad..00000000 --- a/src/components/modules/EndpointModule/EndpointListItem/test.tsx +++ /dev/null @@ -1,58 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TestWrapper from "@src/utils/TestWrapper"; -import EndpointListItem from "."; - -const wrap = props => - new TestWrapper(shallow(), "endpointListItem"); - -describe("EndpointListItem Component", () => { - it("renders item properties", () => { - const wrapper = wrap({ - item: { name: "name-to-test", description: "description-to-test" }, - getUsage: () => { - return {}; - }, - }); - expect(wrapper.findText("name")).toBe("name-to-test"); - expect(wrapper.findText("description")).toBe("description-to-test"); - }); - - it("renders usage count", () => { - const wrapper = wrap({ - item: {}, - getUsage: () => { - return { replicasCount: 12, migrationsCount: 11 }; - }, - }); - expect(wrapper.findText("usageCount")).toBe("11 migrations, 12 replicas"); - }); - - it("dispatches onClick", () => { - const onClick = sinon.spy(); - const wrapper = wrap({ - item: { name: "t" }, - getUsage: () => { - return {}; - }, - onClick, - }); - wrapper.find("content-t").simulate("click"); - expect(onClick.calledOnce).toBe(true); - }); -}); diff --git a/src/components/modules/EndpointModule/EndpointLogos/EndpointLogos.spec.tsx b/src/components/modules/EndpointModule/EndpointLogos/EndpointLogos.spec.tsx new file mode 100644 index 00000000..97dde15e --- /dev/null +++ b/src/components/modules/EndpointModule/EndpointLogos/EndpointLogos.spec.tsx @@ -0,0 +1,58 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import EndpointLogos from "./EndpointLogos"; + +jest.mock("@src/utils/Config", () => ({ + config: { + providerSortPriority: {}, + providerNames: { + openstack: "OpenStack", + vmware_vsphere: "VMware vSphere", + }, + }, +})); + +describe("EndpointLogos", () => { + let defaultProps: EndpointLogos["props"]; + + beforeEach(() => { + defaultProps = { + endpoint: "openstack", + height: 64, + }; + }); + + it("renders without crashing", () => { + render(); + expect(TestUtils.select("EndpointLogos__Logo")).toBeTruthy(); + }); + + it("renders generic logo", () => { + const { getByText } = render( + + ); + expect(getByText("new-endpoint")).toBeTruthy(); + }); + + it("doesn't render for unsupported height", () => { + render(); + expect(TestUtils.select("EndpointLogos__Logo")).toBeFalsy(); + }); +}); diff --git a/src/components/modules/EndpointModule/EndpointLogos/resources/Generic.spec.tsx b/src/components/modules/EndpointModule/EndpointLogos/resources/Generic.spec.tsx new file mode 100644 index 00000000..2bc5ce2f --- /dev/null +++ b/src/components/modules/EndpointModule/EndpointLogos/resources/Generic.spec.tsx @@ -0,0 +1,89 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { ThemePalette } from "@src/components/Theme"; +import { render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import Generic from "./Generic"; + +describe("Generic", () => { + let defaultProps: Generic["props"]; + + beforeEach(() => { + defaultProps = { + name: "Generic", + size: { w: 64, h: 64 }, + disabled: false, + white: false, + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(defaultProps.name)).toBeTruthy(); + }); + + it.each` + height | expectedFontSize + ${32} | ${"14px"} + ${42} | ${"18px"} + `( + "renders with height $height and font size $expectedFontSize", + ({ height, expectedFontSize }) => { + render(); + const wrapper = TestUtils.select("Generic__Wrapper")!; + const style = window.getComputedStyle(wrapper); + expect(style.fontSize).toBe(expectedFontSize); + } + ); + + it.each` + height | expectedLogoWidth | expectedLogoHeight + ${64} | ${"49px"} | ${"43px"} + ${128} | ${"80px"} | ${"70px"} + `( + "renders with height $height and logo width $expectedLogoWidth and logo height $expectedLogoHeight", + ({ height, expectedLogoWidth, expectedLogoHeight }) => { + render(); + const wrapper = TestUtils.select("Generic__Logo")!; + const style = window.getComputedStyle(wrapper); + expect(style.maxWidth).toBe(expectedLogoWidth); + expect(style.maxHeight).toBe(expectedLogoHeight); + } + ); + + it("renders 32px with white color", () => { + render(); + const wrapper = TestUtils.select("Generic__Wrapper")!; + const style = window.getComputedStyle(wrapper); + expect(style.color).toBe("white"); + }); + + it("renders 128px with disabled color", () => { + render(); + const wrapper = TestUtils.select("Generic__Wrapper")!; + const style = window.getComputedStyle(wrapper); + expect(TestUtils.rgbToHex(style.color)).toBe(ThemePalette.grayscale[3]); + }); + + it("doesn't render unsupported size", () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/src/components/modules/EndpointModule/EndpointLogos/test.tsx b/src/components/modules/EndpointModule/EndpointLogos/test.tsx deleted file mode 100644 index 55f12ba5..00000000 --- a/src/components/modules/EndpointModule/EndpointLogos/test.tsx +++ /dev/null @@ -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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TestWrapper from "@src/utils/TestWrapper"; -import EndpointLogos from "."; - -const wrap = props => - new TestWrapper(shallow(), "endpointLogos"); - -describe("EndpointLogos Component", () => { - it("renders 32px aws", () => { - const wrapper = wrap({ height: 32, endpoint: "aws" }); - const logo = wrapper.find("logo"); - expect(logo.prop("url")).toBe("/api/logos/aws/32"); - }); - - it("renders 128px azure disabled", () => { - const wrapper = wrap({ height: 128, endpoint: "azure", disabled: true }); - const logo = wrapper.find("logo"); - expect(logo.prop("url")).toBe("/api/logos/azure/128/disabled"); - }); - - it("renders 64px generic logo", () => { - const wrapper = wrap({ height: 64, endpoint: "generic" }); - const logo = wrapper.find("genericLogo"); - expect(logo.prop("name")).toBe("generic"); - expect(logo.prop("size").h).toBe(64); - }); -}); diff --git a/src/components/modules/EndpointModule/EndpointValidation/EndpointValidation.spec.tsx b/src/components/modules/EndpointModule/EndpointValidation/EndpointValidation.spec.tsx new file mode 100644 index 00000000..2da2dca4 --- /dev/null +++ b/src/components/modules/EndpointModule/EndpointValidation/EndpointValidation.spec.tsx @@ -0,0 +1,117 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import DomUtils from "@src/utils/DomUtils"; +import { render } from "@testing-library/react"; + +import EndpointValidation from "./EndpointValidation"; + +jest.mock("@src/components/ui/StatusComponents/StatusImage", () => ({ + __esModule: true, + default: (props: any) => ( +
+ Status: {props.status || "-"}, Loading: {String(props.loading || false)} +
+ ), +})); + +jest.mock("@src/utils/DomUtils", () => ({ + copyTextToClipboard: jest.fn(), +})); + +describe("EndpointValidation", () => { + let defaultProps: EndpointValidation["props"]; + + beforeEach(() => { + defaultProps = { + loading: false, + validation: { + valid: true, + message: "", + }, + onCancelClick: jest.fn(), + onRetryClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText, getByTestId } = render( + + ); + expect(getByText("Endpoint is Valid")).toBeTruthy(); + expect(getByTestId("StatusImage").textContent).toBe( + "Status: COMPLETED, Loading: false" + ); + }); + + it("renders loading", () => { + const { getByTestId, getByText } = render( + + ); + expect(getByTestId("StatusImage").textContent).toBe( + "Status: -, Loading: true" + ); + expect(getByText("Validating Endpoint")).toBeTruthy(); + }); + + it("renders failed validation", () => { + const { getByTestId, getByText } = render( + + ); + expect(getByTestId("StatusImage").textContent).toBe( + "Status: ERROR, Loading: false" + ); + expect(getByText("connection error")).toBeTruthy(); + }); + + it("renders generic error message", () => { + const { getByTestId, getByText } = render( + + ); + expect(getByTestId("StatusImage").textContent).toBe( + "Status: ERROR, Loading: false" + ); + expect(getByText("An unexpected error occurred.")).toBeTruthy(); + }); + + it("copies the error message to clipboard", () => { + const { getByText } = render( + + ); + getByText("connection error").click(); + expect(DomUtils.copyTextToClipboard).toHaveBeenCalledWith( + "connection error" + ); + }); +}); diff --git a/src/components/modules/EndpointModule/EndpointValidation/test.tsx b/src/components/modules/EndpointModule/EndpointValidation/test.tsx deleted file mode 100644 index 7aacb0d7..00000000 --- a/src/components/modules/EndpointModule/EndpointValidation/test.tsx +++ /dev/null @@ -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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import EndpointValidation from "."; - -const wrap = props => - new TW(shallow(), "eValidation"); - -describe("EndpointValidation Component", () => { - it("renders loading", () => { - const wrapper = wrap({ loading: true }); - expect(wrapper.find("status").prop("loading")).toBe(true); - expect(wrapper.findText("title")).toBe("Validating Endpoint"); - }); - - it("renders valid", () => { - const wrapper = wrap({ validation: { valid: true } }); - expect(wrapper.find("status").prop("status")).toBe("COMPLETED"); - expect(wrapper.findText("title")).toBe("Endpoint is Valid"); - }); - - it("renders failed with default message", () => { - const wrapper = wrap({ validation: {} }); - expect(wrapper.find("status").prop("status")).toBe("ERROR"); - expect(wrapper.findText("title")).toBe("Validation Failed"); - expect(wrapper.findText("errorMessage")).toBe( - "An unexpected error occurred." - ); - }); - - it("renders failed with custom message", () => { - const wrapper = wrap({ validation: { message: "custom_message" } }); - expect(wrapper.find("status").prop("status")).toBe("ERROR"); - expect(wrapper.findText("title")).toBe("Validation Failed"); - expect(wrapper.findText("errorMessage")).toBe( - "custom_message" - ); - }); -}); diff --git a/src/components/modules/LicenceModule/LicenceModule.spec.tsx b/src/components/modules/LicenceModule/LicenceModule.spec.tsx new file mode 100644 index 00000000..185e5155 --- /dev/null +++ b/src/components/modules/LicenceModule/LicenceModule.spec.tsx @@ -0,0 +1,167 @@ +/* +Copyright (C) 2023 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 . +*/ + +import { DateTime } from "luxon"; +import React from "react"; + +import { Licence, LicenceServerStatus } from "@src/@types/Licence"; +import { fireEvent, render, waitFor } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import LicenceModule from "./LicenceModule"; + +jest.mock("@src/components/ui/StatusComponents/StatusImage", () => ({ + __esModule: true, + default: (props: any) => ( +
+ {props.loading ? "loading" : props.status} +
+ ), +})); + +const FUTURE_LICENCE: Licence = { + applianceId: "test-id", + earliestLicenceExpiryDate: DateTime.now().plus({ years: 1 }).toJSDate(), + latestLicenceExpiryDate: DateTime.now().plus({ years: 1 }).toJSDate(), + currentPerformedReplicas: 5, + currentPerformedMigrations: 3, + lifetimePerformedMigrations: 4, + lifetimePerformedReplicas: 6, + currentAvailableReplicas: 10, + currentAvailableMigrations: 5, + lifetimeAvailableReplicas: 15, + lifetimeAvailableMigrations: 10, +}; + +const SERVER_STATUS: LicenceServerStatus = { + hostname: "test-hostname", + multi_appliance: false, + supported_licence_versions: ["v2"], + server_local_time: DateTime.now().toISO()!, +}; + +describe("LicenceModule", () => { + let defaultProps: LicenceModule["props"]; + + beforeEach(() => { + defaultProps = { + licenceInfo: FUTURE_LICENCE, + licenceServerStatus: SERVER_STATUS, + licenceError: null, + loadingLicenceInfo: false, + addMode: false, + addingLicence: false, + backButtonText: "Back", + onAddLicence: jest.fn(), + onRequestClose: jest.fn(), + onAddModeChange: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + getByText("test-id-licencev2"); + }); + + it("changes to add mode when add button is clicked", () => { + const { getByText } = render(); + getByText("Add Licence").click(); + expect(defaultProps.onAddModeChange).toHaveBeenCalledWith(true); + }); + + it("renders add mode", () => { + const { getByText } = render(); + expect(getByText("Drag the Licence file", { exact: false })).toBeTruthy(); + }); + + it("validates invalid licence", async () => { + const { getByText } = render(); + fireEvent.change(document.querySelector("textarea")!, { + target: { value: "test" }, + }); + await waitFor(() => { + const addLicenceButton = getByText("Add Licence"); + expect(addLicenceButton.hasAttribute("disabled")).toBeTruthy(); + }); + }); + + it("validates valid licence", async () => { + const { getByText } = render(); + fireEvent.change(document.querySelector("textarea")!, { + target: { + value: `-----BEGIN CORIOLIS LICENCE----- +Version: 2.0 +-----END CORIOLIS LICENCE-----`, + }, + }); + await waitFor(() => { + const addLicenceButton = getByText("Add Licence"); + expect(addLicenceButton.hasAttribute("disabled")).toBeFalsy(); + }); + }); + + it("shows loading", () => { + const { getByTestId } = render( + + ); + expect(getByTestId("StatusImage").textContent).toBe("loading"); + }); + + it("shows licence expires today", () => { + render( + + ); + + expect( + TestUtils.selectAll("LicenceModule__LicenceRowDescription")[0].textContent + ).toContain("today at"); + }); + + it("shows licence expired", () => { + render( + + ); + + expect( + TestUtils.select("LicenceModule__LicenceRowContent")?.textContent + ).toContain("Please contact Cloudbase Solutions"); + }); + + it("renders licence error", () => { + const { getByText } = render( + + ); + expect(getByText("test-error")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/LoginModule/LoginForm/LoginForm.spec.tsx b/src/components/modules/LoginModule/LoginForm/LoginForm.spec.tsx new file mode 100644 index 00000000..8667d33c --- /dev/null +++ b/src/components/modules/LoginModule/LoginForm/LoginForm.spec.tsx @@ -0,0 +1,93 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import notificationStore from "@src/stores/NotificationStore"; +import { fireEvent, render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import LoginForm from "./LoginForm"; + +jest.mock("@src/stores/NotificationStore", () => ({ + alert: jest.fn(), +})); + +describe("LoginForm", () => { + let defaultProps: LoginForm["props"]; + + beforeEach(() => { + defaultProps = { + className: "class-custom-name", + showUserDomainInput: false, + loading: false, + loginFailedResponse: null, + domain: "default", + onDomainChange: jest.fn(), + onFormSubmit: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Username")).toBeTruthy(); + expect(getByText("Password")).toBeTruthy(); + }); + + it("submits username and password", () => { + render(); + const userInput = TestUtils.selectAll("TextInput__Input")[0]; + const passwordInput = TestUtils.selectAll("TextInput__Input")[1]; + fireEvent.change(userInput, { target: { value: "username" } }); + fireEvent.change(passwordInput, { target: { value: "password" } }); + fireEvent.submit(document.querySelector("form")!); + expect(defaultProps.onFormSubmit).toHaveBeenCalledWith({ + username: "username", + password: "password", + }); + }); + + it("submits domain", () => { + render(); + const domainInput = TestUtils.selectAll("TextInput__Input")[0]; + expect((domainInput as HTMLInputElement).value).toBe(defaultProps.domain); + fireEvent.change(domainInput, { target: { value: "new-domain" } }); + expect(defaultProps.onDomainChange).toHaveBeenCalledWith("new-domain"); + }); + + it("shows fill all fields error", () => { + render(); + fireEvent.submit(document.querySelector("form")!); + expect(notificationStore.alert).toHaveBeenCalledWith( + "Please fill in all fields" + ); + }); + + it("renders incorrect crediantials message", () => { + const { getByText } = render( + + ); + expect(getByText("Incorrect credentials", { exact: false })).toBeTruthy(); + }); + + it("renders other error message", () => { + const { getByText } = render( + + ); + expect(getByText("other error", { exact: false })).toBeTruthy(); + }); +}); diff --git a/src/components/modules/LoginModule/LoginForm/LoginForm.tsx b/src/components/modules/LoginModule/LoginForm/LoginForm.tsx index 04684bca..31c4e8d7 100644 --- a/src/components/modules/LoginModule/LoginForm/LoginForm.tsx +++ b/src/components/modules/LoginModule/LoginForm/LoginForm.tsx @@ -87,7 +87,7 @@ type Props = { className: string; showUserDomainInput: boolean; loading: boolean; - loginFailedResponse: { status: string | number; message?: string }; + loginFailedResponse: { status: string | number; message?: string } | null; domain: string; onDomainChange: (domain: string) => void; onFormSubmit: (credentials: { username: string; password: string }) => void; diff --git a/src/components/modules/LoginModule/LoginForm/test.tsx b/src/components/modules/LoginModule/LoginForm/test.tsx deleted file mode 100644 index 3d407573..00000000 --- a/src/components/modules/LoginModule/LoginForm/test.tsx +++ /dev/null @@ -1,54 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import LoginForm from "."; - -const wrap = props => - new TW(shallow(), "loginForm"); - -describe("LoginForm Component", () => { - it("renders incorrect credentials", () => { - const wrapper = wrap({ loginFailedResponse: { status: 401 } }); - expect( - wrapper.find("errorText").prop("dangerouslySetInnerHTML").__html - ).toBe("Incorrect credentials.
Please try again."); // eslint-disable-line - }); - - it("renders server error", () => { - const wrapper = wrap({ loginFailedResponse: {} }); - expect( - wrapper.find("errorText").prop("dangerouslySetInnerHTML").__html - ).toBe( - "Request failed, there might be a problem with the connection to the server." - ); // eslint-disable-line - }); - - it("submits correct info", () => { - const onFormSubmit = sinon.spy(); - const wrapper = wrap({ onFormSubmit }); - wrapper - .find("usernameField") - .simulate("change", { target: { value: "usr" } }); - wrapper - .find("passwordField") - .simulate("change", { target: { value: "pswd" } }); - wrapper.shallow.simulate("submit", { preventDefault: () => {} }); - expect(onFormSubmit.args[0][0].username).toBe("usr"); - expect(onFormSubmit.args[0][0].password).toBe("pswd"); - }); -}); diff --git a/src/components/modules/LoginModule/LoginFormField/test.tsx b/src/components/modules/LoginModule/LoginFormField/test.tsx deleted file mode 100644 index 94ce93cb..00000000 --- a/src/components/modules/LoginModule/LoginFormField/test.tsx +++ /dev/null @@ -1,36 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TestWrapper from "@src/utils/TestWrapper"; -import LoginFormField from "."; - -const wrap = props => - new TestWrapper(shallow(), "loginFormField"); - -describe("LoginFormField Component", () => { - it("renders with correct label", () => { - const wrapper = wrap({ label: "Username" }); - expect(wrapper.findText("label")).toBe("Username"); - }); - - it("dispatches change on input change", () => { - const onChange = sinon.spy(); - const wrapper = wrap({ label: "Username", onChange }); - wrapper.find("input").simulate("change", { t: "t" }); - expect(onChange.args[0][0].t).toBe("t"); - }); -}); diff --git a/src/components/modules/LoginModule/LoginOptions/LoginOptions.spec.tsx b/src/components/modules/LoginModule/LoginOptions/LoginOptions.spec.tsx new file mode 100644 index 00000000..baaaa4d7 --- /dev/null +++ b/src/components/modules/LoginModule/LoginOptions/LoginOptions.spec.tsx @@ -0,0 +1,59 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; + +import LoginOptions, { Props } from "./LoginOptions"; + +describe("LoginForm", () => { + let defaultProps: Props; + + beforeEach(() => { + defaultProps = { + buttons: [ + { + id: "google", + name: "Google", + }, + { + id: "microsoft", + name: "Microsoft", + }, + { + id: "facebook", + name: "Facebook", + }, + { + id: "github", + name: "GitHub", + }, + ], + }; + }); + + it("renders without crashing", () => { + render(); + expect(document.querySelectorAll("div").length).toBe(1); + }); + + it("renders all buttons", () => { + const { getByText } = render(); + expect(getByText("Sign in with Google")).toBeTruthy(); + expect(getByText("Sign in with Microsoft")).toBeTruthy(); + expect(getByText("Sign in with Facebook")).toBeTruthy(); + expect(getByText("Sign in with GitHub")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/LoginModule/LoginOptions/LoginOptions.tsx b/src/components/modules/LoginModule/LoginOptions/LoginOptions.tsx index 751c1688..7817204b 100644 --- a/src/components/modules/LoginModule/LoginOptions/LoginOptions.tsx +++ b/src/components/modules/LoginModule/LoginOptions/LoginOptions.tsx @@ -97,7 +97,7 @@ const Logo = styled.div` margin: 0 8px 0 8px; ${props => buttonStyle(props.id, true)} `; -type Props = { +export type Props = { buttons?: { name: string; id: string }[]; }; const LoginOptions = (props: Props) => { diff --git a/src/components/modules/LoginModule/LoginOptions/test.tsx b/src/components/modules/LoginModule/LoginOptions/test.tsx deleted file mode 100644 index 04464ac3..00000000 --- a/src/components/modules/LoginModule/LoginOptions/test.tsx +++ /dev/null @@ -1,57 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TestWrapper from "@src/utils/TestWrapper"; -import LoginOptions from "."; - -const wrap = props => - new TestWrapper(shallow(), "loginOptions"); - -const buttons = [ - { - name: "Google", - id: "google", - url: "", - }, - { - name: "Microsoft", - id: "microsoft", - url: "", - }, - { - name: "Facebook", - id: "facebook", - url: "", - }, - { - name: "GitHub", - id: "github", - url: "", - }, -]; - -describe("LoginOptions Component", () => { - it("renders with all buttons", () => { - const wrapper = wrap({ buttons }); - expect(wrapper.findPartialId("button").length).toBe(4); - buttons.forEach(button => { - expect(wrapper.findText(`button-${button.id}`)).toBe( - `Sign in with ${button.name}` - ); - expect(wrapper.find(`logo-${button.id}`).prop("id")).toBe(button.id); - }); - }); -}); diff --git a/src/components/modules/MetalHubModule/MetalHubListHeader/MetalHubListHeader.spec.tsx b/src/components/modules/MetalHubModule/MetalHubListHeader/MetalHubListHeader.spec.tsx new file mode 100644 index 00000000..85816eec --- /dev/null +++ b/src/components/modules/MetalHubModule/MetalHubListHeader/MetalHubListHeader.spec.tsx @@ -0,0 +1,39 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; + +import MetalHubListHeader from "./MetalHubListHeader"; + +describe("MetalHubListHeader", () => { + let defaultProps: MetalHubListHeader["props"]; + + beforeEach(() => { + defaultProps = { + hideButton: false, + error: "", + fingerprint: "e4:95:6e:5c:2a:11:d4:1f:a2", + visible: true, + onCreateClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("e4:95:6e:5c:...:11:d4:1f:a2")).toBeTruthy(); + expect(getByText("Add a Bare Metal Server")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/MetalHubModule/MetalHubListItem/MetalHubListItem.spec.tsx b/src/components/modules/MetalHubModule/MetalHubListItem/MetalHubListItem.spec.tsx new file mode 100644 index 00000000..8a3855aa --- /dev/null +++ b/src/components/modules/MetalHubModule/MetalHubListItem/MetalHubListItem.spec.tsx @@ -0,0 +1,65 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import DateUtils from "@src/utils/DateUtils"; +import { render } from "@testing-library/react"; +import { METALHUB_SERVER_MOCK } from "@tests/mocks/MetalHubServerMock"; + +import MetalHubListItem from "./MetalHubListItem"; + +describe("MetalHubListItem", () => { + let defaultProps: MetalHubListItem["props"]; + + beforeEach(() => { + defaultProps = { + item: METALHUB_SERVER_MOCK, + selected: false, + onSelectedChange: jest.fn(), + onClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText, getAllByText } = render( + + ); + expect(getByText(METALHUB_SERVER_MOCK.hostname!)).toBeTruthy(); + expect(getByText("Active")).toBeTruthy(); + expect(getByText(METALHUB_SERVER_MOCK.api_endpoint)).toBeTruthy(); + expect( + getAllByText( + DateUtils.getLocalDate(METALHUB_SERVER_MOCK.created_at).toFormat( + "yyyy-LL-dd HH:mm:ss" + ) + ).length + ).toBe(2); + }); + + it("renders default hostname when hostname is empty and inactive status", () => { + const { getByText } = render( + + ); + expect(getByText("No Hostname")).toBeTruthy(); + expect(getByText("Inactive")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/MetalHubModule/MetalHubModal/MetalHubModal.spec.tsx b/src/components/modules/MetalHubModule/MetalHubModal/MetalHubModal.spec.tsx new file mode 100644 index 00000000..14062834 --- /dev/null +++ b/src/components/modules/MetalHubModule/MetalHubModal/MetalHubModal.spec.tsx @@ -0,0 +1,126 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import metalHubStore from "@src/stores/MetalHubStore"; +import { fireEvent, render, waitFor } from "@testing-library/react"; +import { METALHUB_SERVER_MOCK } from "@tests/mocks/MetalHubServerMock"; + +import MetalHubModal from "./MetalHubModal"; + +describe("MetalHubModal", () => { + let defaultProps: MetalHubModal["props"]; + let metalHubStoreSpies: { + clearValidationError: jest.SpyInstance; + patchServer: jest.SpyInstance; + validateServer: jest.SpyInstance; + addServer: jest.SpyInstance; + }; + + beforeEach(() => { + metalHubStoreSpies = { + clearValidationError: jest.spyOn(metalHubStore, "clearValidationError"), + patchServer: jest.spyOn(metalHubStore, "patchServer").mockResolvedValue(), + validateServer: jest + .spyOn(metalHubStore, "validateServer") + .mockResolvedValue(true), + addServer: jest.spyOn(metalHubStore, "addServer").mockResolvedValue({ + ...METALHUB_SERVER_MOCK, + }), + }; + defaultProps = { + onRequestClose: jest.fn(), + onUpdateDone: jest.fn(), + }; + }); + + it("renders without crashing and clears validation error on unmount", () => { + const { getByText, unmount } = render(); + expect(getByText("Add Coriolis Bare Metal Server")).toBeTruthy(); + unmount(); + expect(metalHubStoreSpies.clearValidationError).toHaveBeenCalledTimes(1); + }); + + it("shows the server for editing", () => { + const { getByText } = render( + + ); + expect(getByText("Update Coriolis Bare Metal Server")).toBeTruthy(); + const testInput = (label: string, value: string) => { + const input = + getByText(label).parentElement!.parentElement!.querySelector("input"); + expect(input).toBeTruthy(); + expect(input!.value).toBe(value); + }; + testInput( + "Host", + METALHUB_SERVER_MOCK.api_endpoint!.split(":")[1].replace("//", "") + ); + testInput( + "Port", + METALHUB_SERVER_MOCK.api_endpoint!.split(":")[2].replace(/\/.*/, "") + ); + }); + + it("renders validation error", () => { + metalHubStore.validationError = ["Validation error", "Validation error 2"]; + const { getByText } = render( + + ); + expect(getByText("Validation error")).toBeTruthy(); + expect(getByText("Validation error 2")).toBeTruthy(); + metalHubStore.validationError = []; + }); + + it("triggers submit on enter key", async () => { + render( + + ); + const input = document.querySelector("input")!; + fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); + await waitFor(() => { + expect(metalHubStoreSpies.patchServer).toHaveBeenCalledTimes(1); + }); + expect(metalHubStoreSpies.validateServer).toHaveBeenCalledTimes(1); + }); + + it("highlights invalid fields", () => { + const { getByText, getAllByText } = render( + + ); + fireEvent.click(getByText("Validate and save")); + expect(getAllByText("Required field").length).toBeGreaterThan(0); + }); + + it("adds new server", async () => { + const { getByText } = render(); + const getInput = (label: string): HTMLInputElement => + getByText(label).parentElement!.parentElement!.querySelector("input")!; + + fireEvent.change(getInput("Host"), { + target: { value: "api.example.com" }, + }); + fireEvent.change(getInput("Port"), { target: { value: "5566" } }); + fireEvent.click(getByText("Validate and save")); + await waitFor(() => { + expect(metalHubStoreSpies.addServer).toHaveBeenCalledWith( + "https://api.example.com:5566/api/v1" + ); + }); + expect(metalHubStoreSpies.validateServer).toHaveBeenCalledWith( + METALHUB_SERVER_MOCK.id + ); + }); +}); diff --git a/src/components/modules/MetalHubModule/MetalHubModal/MetalHubModal.tsx b/src/components/modules/MetalHubModule/MetalHubModal/MetalHubModal.tsx index 05127df4..a2e36b07 100644 --- a/src/components/modules/MetalHubModule/MetalHubModal/MetalHubModal.tsx +++ b/src/components/modules/MetalHubModule/MetalHubModal/MetalHubModal.tsx @@ -12,22 +12,23 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled, { css } from "styled-components"; -import type { Field as FieldType } from "@src/@types/Field"; +import { MetalHubServer } from "@src/@types/MetalHub"; +import { ThemeProps } from "@src/components/Theme"; import Button from "@src/components/ui/Button"; -import Modal from "@src/components/ui/Modal"; import FieldInput from "@src/components/ui/FieldInput"; - -import KeyboardManager from "@src/utils/KeyboardManager"; -import { ThemeProps } from "@src/components/Theme"; import LoadingButton from "@src/components/ui/LoadingButton"; -import { MetalHubServer } from "@src/@types/MetalHub"; -import image from "./images/server.svg"; -import metalHubStore from "@src/stores/MetalHubStore"; +import Modal from "@src/components/ui/Modal"; import StatusIcon from "@src/components/ui/StatusComponents/StatusIcon"; +import metalHubStore from "@src/stores/MetalHubStore"; +import KeyboardManager from "@src/utils/KeyboardManager"; + +import image from "./images/server.svg"; + +import type { Field as FieldType } from "@src/@types/Field"; const Wrapper = styled.div` padding: 48px 32px 32px 32px; @@ -302,7 +303,7 @@ class MetalHubModal extends React.Component { const message = this.state.saving ? "Validating ..." : metalHubStore.validationError.length - ? metalHubStore.validationError.map(e =>
{e}
) + ? metalHubStore.validationError.map(e =>
{e}
) : "Validation successful"; const status = this.state.saving ? "RUNNING" diff --git a/src/components/modules/MetalHubModule/MetalHubServerDetailsContent/MetalHubServerDetailsContent.spec.tsx b/src/components/modules/MetalHubModule/MetalHubServerDetailsContent/MetalHubServerDetailsContent.spec.tsx new file mode 100644 index 00000000..503ca0fc --- /dev/null +++ b/src/components/modules/MetalHubModule/MetalHubServerDetailsContent/MetalHubServerDetailsContent.spec.tsx @@ -0,0 +1,101 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import DateUtils from "@src/utils/DateUtils"; +import { render } from "@testing-library/react"; +import { METALHUB_SERVER_MOCK } from "@tests/mocks/MetalHubServerMock"; +import TestUtils from "@tests/TestUtils"; + +import MetalHubServerDetailsContent from "./MetalHubServerDetailsContent"; + +jest.mock("@src/components/ui/Arrow", () => ({ + __esModule: true, + default: (props: any) => ( +
Orientation: {props.orientation}
+ ), +})); + +describe("MetalHubServerDetailsContent", () => { + let defaultProps: MetalHubServerDetailsContent["props"]; + + beforeEach(() => { + defaultProps = { + server: { ...METALHUB_SERVER_MOCK }, + loading: false, + creatingMigration: false, + creatingReplica: false, + onCreateReplicaClick: jest.fn(), + onCreateMigrationClick: jest.fn(), + onDeleteClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + render(); + const getText = (text: string) => { + let element: Element | null = null; + document.querySelectorAll("*").forEach(el => { + if (el.textContent && el.textContent.includes(text)) { + element = el; + } + }); + if (!element) throw new Error(`Element with text "${text}" not found`); + return element; + }; + expect(getText(METALHUB_SERVER_MOCK.hostname!)).toBeTruthy(); + expect(getText(METALHUB_SERVER_MOCK.api_endpoint)).toBeTruthy(); + expect( + getText( + DateUtils.getLocalDate(METALHUB_SERVER_MOCK.created_at).toFormat( + "yyyy-LL-dd HH:mm:ss" + ) + ) + ).toBeTruthy(); + + expect( + getText(`${METALHUB_SERVER_MOCK.physical_cores} physical`) + ).toBeTruthy(); + expect( + getText(`${METALHUB_SERVER_MOCK.logical_cores} logical`) + ).toBeTruthy(); + expect( + getText( + `${METALHUB_SERVER_MOCK.os_info.os_name} ${METALHUB_SERVER_MOCK.os_info.os_version}` + ) + ).toBeTruthy(); + }); + + it("handles row click", () => { + const { getAllByTestId } = render( + + ); + const row = TestUtils.select("TransferDetailsTable__Row-")!; + expect(row).toBeTruthy(); + expect(getAllByTestId("Arrow")[0].textContent).toBe("Orientation: down"); + row.click(); + expect(getAllByTestId("Arrow")[0].textContent).toBe("Orientation: up"); + + row.click(); + expect(getAllByTestId("Arrow")[0].textContent).toBe("Orientation: down"); + }); + + it("renders loading", () => { + render(); + expect( + TestUtils.select("MetalHubServerDetailsContent__LoadingWrapper") + ).toBeTruthy(); + }); +}); diff --git a/src/components/modules/MinionModule/MinionEndpointModal/MinionEndpointModal.spec.tsx b/src/components/modules/MinionModule/MinionEndpointModal/MinionEndpointModal.spec.tsx new file mode 100644 index 00000000..03cc0202 --- /dev/null +++ b/src/components/modules/MinionModule/MinionEndpointModal/MinionEndpointModal.spec.tsx @@ -0,0 +1,92 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { + OPENSTACK_ENDPOINT_MOCK, + VMWARE_ENDPOINT_MOCK, +} from "@tests/mocks/EndpointsMock"; +import { PROVIDERS_MOCK } from "@tests/mocks/ProvidersMock"; +import TestUtils from "@tests/TestUtils"; + +import MinionEndpointModal from "./MinionEndpointModal"; + +jest.mock("@src/components/modules/EndpointModule/EndpointLogos", () => ({ + __esModule: true, + default: (props: any) => ( +
{props.endpoint}
+ ), +})); + +jest.mock("react-transition-group", () => ({ + CSSTransitionGroup: (props: any) =>
{props.children}
, +})); + +describe("MinionEndpointModal", () => { + let defaultProps: MinionEndpointModal["props"]; + + beforeEach(() => { + defaultProps = { + providers: PROVIDERS_MOCK, + endpoints: [OPENSTACK_ENDPOINT_MOCK, VMWARE_ENDPOINT_MOCK], + loading: false, + onRequestClose: jest.fn(), + onSelectEndpoint: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByTestId } = render(); + expect(getByTestId("EndpointLogos").textContent).toBe("vmware_vsphere"); + }); + + it("renders no endpoints if provider doesn't have support for minions", () => { + render( + + ); + expect( + TestUtils.select("MinionEndpointModal__NoEndpoints")?.textContent + ).toContain("Please create a Coriolis Endpoint"); + }); + + it("renders no endpoints if no providers", () => { + render(); + expect( + TestUtils.select("MinionEndpointModal__NoEndpoints")?.textContent + ).toContain("Please create a Coriolis Endpoint"); + }); + + it("selects an endpoint", () => { + const { getByText } = render(); + TestUtils.select("DropdownButton__Wrapper")?.click(); + TestUtils.select("Dropdown__ListItem-")?.click(); + getByText("Next").click(); + expect(defaultProps.onSelectEndpoint).toHaveBeenCalledWith( + VMWARE_ENDPOINT_MOCK, + "source" + ); + }); + + it("renders loading", () => { + render(); + expect( + TestUtils.select("MinionEndpointModal__LoadingWrapper") + ).toBeTruthy(); + }); +}); diff --git a/src/components/modules/MinionModule/MinionEndpointModal/MinionEndpointModal.tsx b/src/components/modules/MinionModule/MinionEndpointModal/MinionEndpointModal.tsx index 1d55cc1d..4e6bef15 100644 --- a/src/components/modules/MinionModule/MinionEndpointModal/MinionEndpointModal.tsx +++ b/src/components/modules/MinionModule/MinionEndpointModal/MinionEndpointModal.tsx @@ -183,7 +183,7 @@ class MinionEndpointModal extends React.Component { : providerTypes.DESTINATION_MINION_POOL; const types = this.props.providers?.[providerName].types.indexOf(providerType); - return types && types > -1; + return types != null && types > -1; } ); diff --git a/src/components/modules/MinionModule/MinionPoolConfirmationModal/MinionPoolConfirmationModal.spec.tsx b/src/components/modules/MinionModule/MinionPoolConfirmationModal/MinionPoolConfirmationModal.spec.tsx new file mode 100644 index 00000000..60dacc3b --- /dev/null +++ b/src/components/modules/MinionModule/MinionPoolConfirmationModal/MinionPoolConfirmationModal.spec.tsx @@ -0,0 +1,75 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { fireEvent, render } from "@testing-library/react"; + +import MinionPoolConfirmationModal from "./MinionPoolConfirmationModal"; + +jest.mock("@src/components/ui/FieldInput", () => ({ + __esModule: true, + default: (props: any) => ( +
{ + props.onChange(true); + }} + > + {props.label} - {String(props.value)} +
+ ), +})); + +describe("MinionPoolConfirmationModal", () => { + let defaultProps: MinionPoolConfirmationModal["props"]; + + beforeEach(() => { + defaultProps = { + onCancelClick: jest.fn(), + onExecuteClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + render(); + let element: Element | null = null; + document.querySelectorAll("*").forEach(el => { + if (el.textContent && el.textContent.includes("Are you sure")) { + element = el; + } + }); + if (!element) throw new Error(`Element not found`); + expect(element).toBeTruthy(); + }); + + it("handles submit on Enter key press", () => { + render(); + + fireEvent.keyDown(document, { key: "Enter" }); + + expect(defaultProps.onExecuteClick).toHaveBeenCalledWith(false); + }); + + it("executes with force flag", () => { + const { getByTestId } = render( + + ); + + fireEvent.click(getByTestId("FieldInput")); + fireEvent.click(document.querySelectorAll("button")[1]); + + expect(defaultProps.onExecuteClick).toHaveBeenCalledWith(true); + }); +}); diff --git a/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolDetailsContent.spec.tsx b/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolDetailsContent.spec.tsx new file mode 100644 index 00000000..6c325f2b --- /dev/null +++ b/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolDetailsContent.spec.tsx @@ -0,0 +1,102 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { OPENSTACK_ENDPOINT_MOCK } from "@tests/mocks/EndpointsMock"; +import { MINION_POOL_DETAILS_MOCK } from "@tests/mocks/MinionPoolMock"; +import { REPLICA_MOCK } from "@tests/mocks/TransferMock"; +import TestUtils from "@tests/TestUtils"; + +import MinionPoolDetailsContent from "./MinionPoolDetailsContent"; + +jest.mock("react-router-dom", () => ({ Link: "a" })); +jest.mock("@src/components/modules/EndpointModule/EndpointLogos", () => ({ + __esModule: true, + default: (props: any) => ( +
{props.endpoint}
+ ), +})); +jest.mock( + "@src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolEvents", + () => ({ + __esModule: true, + default: () =>
, + }) +); + +describe("MinionPoolDetailsContent", () => { + let defaultProps: MinionPoolDetailsContent["props"]; + + beforeEach(() => { + defaultProps = { + item: MINION_POOL_DETAILS_MOCK, + itemId: "minion-pool-id", + replicas: [REPLICA_MOCK], + migrations: [], + endpoints: [OPENSTACK_ENDPOINT_MOCK], + schema: [ + { + name: "name", + label: "Name", + type: "text", + required: true, + disabled: false, + }, + ], + schemaLoading: false, + loading: false, + page: "", + onAllocate: jest.fn(), + onDeleteMinionPoolClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render( + + ); + expect(getByText(MINION_POOL_DETAILS_MOCK.id)).toBeTruthy(); + expect(getByText(MINION_POOL_DETAILS_MOCK.notes!)).toBeTruthy(); + }); + + it("calls allocate callback", () => { + const { getByText } = render( + + ); + getByText("Allocate").click(); + expect(defaultProps.onAllocate).toHaveBeenCalled(); + }); + + it("renders loading", () => { + render(); + expect(TestUtils.select("MinionPoolDetailsContent__Loading")).toBeTruthy(); + }); + + it("renders machines page", () => { + render(); + expect(TestUtils.select("MinionPoolMachines")).toBeTruthy(); + }); + + it("renders events page", () => { + const { getByTestId } = render( + + ); + expect(getByTestId("MinionPoolEvents")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolEvents.spec.tsx b/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolEvents.spec.tsx new file mode 100644 index 00000000..934ea2b7 --- /dev/null +++ b/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolEvents.spec.tsx @@ -0,0 +1,137 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { MINION_POOL_DETAILS_MOCK } from "@tests/mocks/MinionPoolMock"; +import TestUtils from "@tests/TestUtils"; + +import MinionPoolEvents from "./MinionPoolEvents"; + +jest.mock("@src/utils/Config", () => ({ + config: { + maxMinionPoolEventsPerPage: 1, + }, +})); + +describe("MinionPoolEvents", () => { + let defaultProps: MinionPoolEvents["props"]; + + beforeEach(() => { + defaultProps = { + item: MINION_POOL_DETAILS_MOCK, + }; + }); + + it("renders without crashing", () => { + render(); + expect(TestUtils.select("MinionPoolEvents__Message")?.textContent).toBe( + MINION_POOL_DETAILS_MOCK.events[0].message + ); + }); + + it.each` + fromLabel | toLabel + ${"Events"} | ${"Progress Updates"} + ${"Events"} | ${"Events & Progress Updates"} + ${"INFO Event Level"} | ${"DEBUG Event Level"} + ${"INFO Event Level"} | ${"ERROR Event Level"} + ${"Descending Order"} | ${"Ascending Order"} + `("filters by $fromLabel to $toLabel", ({ fromLabel, toLabel }) => { + render(); + let filterDropdown: HTMLElement | null = null; + TestUtils.selectAll("DropdownLink__Label").forEach(element => { + if (element.textContent === fromLabel) { + filterDropdown = element; + } + }); + expect(filterDropdown).toBeTruthy(); + filterDropdown!.click(); + let listItem: HTMLElement | null = null; + TestUtils.selectAll("DropdownLink__ListItem-").forEach(element => { + if (element.textContent === toLabel) { + listItem = element; + } + }); + expect(listItem).toBeTruthy(); + listItem!.click(); + + TestUtils.selectAll("DropdownLink__Label").forEach(element => { + if (element.textContent === toLabel) { + filterDropdown = element; + } + }); + expect(filterDropdown).toBeTruthy(); + }); + + describe("Pagination", () => { + const showAllEvents = () => { + let showAllEvents: HTMLElement | null = null; + TestUtils.selectAll("DropdownLink__Label").forEach(element => { + if (element.textContent === "Events") { + showAllEvents = element; + } + }); + expect(showAllEvents).toBeTruthy(); + showAllEvents!.click(); + let listItem: HTMLElement | null = null; + TestUtils.selectAll("DropdownLink__ListItem-").forEach(element => { + if (element.textContent === "Events & Progress Updates") { + listItem = element; + } + }); + expect(listItem).toBeTruthy(); + listItem!.click(); + }; + + it("has pagination", () => { + render(); + + // pagination is not visible for 1 event + expect(TestUtils.select("Pagination__Wrapper")!).toBeFalsy(); + + showAllEvents(); + + // pagination is visible for more than 1 event + expect(TestUtils.select("Pagination__Wrapper")!).toBeTruthy(); + }); + + it("goes to next page and back", () => { + render(); + + showAllEvents(); + + expect( + TestUtils.select("Pagination__PagePrevious")!.hasAttribute("disabled") + ).toBeTruthy(); + expect(TestUtils.select("Pagination__PageNumber")!.textContent).toBe( + "1 of 3" + ); + + TestUtils.select("Pagination__PageNext")!.click(); + expect( + TestUtils.select("Pagination__PagePrevious")!.hasAttribute("disabled") + ).toBeFalsy(); + expect(TestUtils.select("Pagination__PageNumber")!.textContent).toBe( + "2 of 3" + ); + + TestUtils.select("Pagination__PagePrevious")!.click(); + expect(TestUtils.select("Pagination__PageNumber")!.textContent).toBe( + "1 of 3" + ); + }); + }); +}); diff --git a/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolEvents.tsx b/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolEvents.tsx index e338c67e..611eec42 100644 --- a/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolEvents.tsx +++ b/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolEvents.tsx @@ -93,17 +93,6 @@ type State = { orderDir: OrderDir; }; class MinionPoolEvents extends React.Component { - static sortData( - data: MinionPoolEventProgressUpdate[] - ): MinionPoolEventProgressUpdate[] { - return data - .slice() - .sort( - (a, b) => - new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - ); - } - state = { allEvents: [] as MinionPoolEventProgressUpdate[], prevLenghts: [0, 0], diff --git a/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolMachines.spec.tsx b/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolMachines.spec.tsx new file mode 100644 index 00000000..b1f19668 --- /dev/null +++ b/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolMachines.spec.tsx @@ -0,0 +1,106 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { MINION_POOL_MOCK } from "@tests/mocks/MinionPoolMock"; +import { MIGRATION_MOCK, REPLICA_MOCK } from "@tests/mocks/TransferMock"; +import TestUtils from "@tests/TestUtils"; + +import MinionPoolMachines from "./MinionPoolMachines"; + +jest.mock("react-router-dom", () => ({ Link: "a" })); + +describe("MinionPoolMachines", () => { + let defaultProps: MinionPoolMachines["props"]; + + beforeEach(() => { + defaultProps = { + item: MINION_POOL_MOCK, + replicas: [REPLICA_MOCK], + migrations: [MIGRATION_MOCK], + }; + }); + + const filterBy = (fromLabel: string, toLabel: string) => { + let filterDropdown: HTMLElement | null = null; + TestUtils.selectAll("DropdownLink__Label").forEach(element => { + if (element.textContent === fromLabel) { + filterDropdown = element; + } + }); + expect(filterDropdown).toBeTruthy(); + + filterDropdown!.click(); + + let filterItem: HTMLElement | null = null; + TestUtils.selectAll("DropdownLink__ListItem-").forEach(element => { + if (element.textContent === toLabel) { + filterItem = element; + } + }); + expect(filterItem).toBeTruthy(); + filterItem!.click(); + }; + + it("renders without crashing", () => { + const { getByText } = render(); + expect( + TestUtils.select("MinionPoolMachines__HeaderText")?.textContent + ).toBe("1 minion machine, 1 allocated"); + expect( + getByText(`ID: ${MINION_POOL_MOCK.minion_machines[0].id}`) + ).toBeTruthy(); + }); + + it("filters correctly", () => { + render(); + filterBy("All", "Allocated"); + expect( + TestUtils.selectAll("MinionPoolMachines__MachineWrapper").length + ).toBe(1); + + filterBy("Allocated", "Not Allocated"); + expect( + TestUtils.selectAll("MinionPoolMachines__MachineWrapper").length + ).toBe(0); + }); + + it("renders no machines", () => { + render( + + ); + expect(TestUtils.select("MinionPoolMachines__NoMachines")).toBeTruthy(); + }); + + it("handles row click", () => { + render(); + const arrow = TestUtils.select( + "Arrow__Wrapper", + TestUtils.select("MinionPoolMachines__Row-")! + ); + expect(arrow).toBeTruthy(); + expect(arrow!.attributes.getNamedItem("orientation")!.value).toBe("down"); + + TestUtils.select("MinionPoolMachines__Row-")!.click(); + expect(arrow!.attributes.getNamedItem("orientation")!.value).toBe("up"); + + TestUtils.select("MinionPoolMachines__Row-")!.click(); + expect(arrow!.attributes.getNamedItem("orientation")!.value).toBe("down"); + }); +}); diff --git a/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolMainDetails.spec.tsx b/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolMainDetails.spec.tsx new file mode 100644 index 00000000..9b71a09e --- /dev/null +++ b/src/components/modules/MinionModule/MinionPoolDetailsContent/MinionPoolMainDetails.spec.tsx @@ -0,0 +1,74 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { OPENSTACK_ENDPOINT_MOCK } from "@tests/mocks/EndpointsMock"; +import { MINION_POOL_MOCK } from "@tests/mocks/MinionPoolMock"; +import { MIGRATION_MOCK, REPLICA_MOCK } from "@tests/mocks/TransferMock"; + +import MinionPoolMainDetails from "./MinionPoolMainDetails"; + +jest.mock("react-router-dom", () => ({ Link: "a" })); +jest.mock("@src/components/modules/EndpointModule/EndpointLogos", () => ({ + __esModule: true, + default: (props: any) => ( +
{props.endpoint}
+ ), +})); + +describe("MinionPoolMainDetails", () => { + let defaultProps: MinionPoolMainDetails["props"]; + + beforeEach(() => { + defaultProps = { + item: MINION_POOL_MOCK, + replicas: [REPLICA_MOCK], + migrations: [MIGRATION_MOCK], + schema: [], + schemaLoading: false, + endpoints: [OPENSTACK_ENDPOINT_MOCK], + bottomControls:
BC
, + }; + }); + + it("renders without crashing", () => { + const { getByText, getByTestId } = render( + + ); + expect(getByText(MINION_POOL_MOCK.notes!)).toBeTruthy(); + expect(getByText(OPENSTACK_ENDPOINT_MOCK.name)).toBeTruthy(); + expect(getByTestId("bottom-controls")).toBeTruthy(); + expect( + getByText(MINION_POOL_MOCK.environment_options.option_1) + ).toBeTruthy(); + expect(getByText("Object Option - Object Option 1")).toBeTruthy(); + expect( + getByText(MINION_POOL_MOCK.environment_options.array_option[0]) + ).toBeTruthy(); + expect(getByText("source_value=destination_value")).toBeTruthy(); + }); + + it("renders missing endpoint", () => { + render(); + let missingEndpoint: Element | null = null; + document.querySelectorAll("*").forEach(element => { + if (element.textContent === "Endpoint is missing") { + missingEndpoint = element; + } + }); + expect(missingEndpoint).toBeTruthy(); + }); +}); diff --git a/src/components/modules/MinionModule/MinionPoolListItem/MinionPoolListItem.spec.tsx b/src/components/modules/MinionModule/MinionPoolListItem/MinionPoolListItem.spec.tsx new file mode 100644 index 00000000..d9e38412 --- /dev/null +++ b/src/components/modules/MinionModule/MinionPoolListItem/MinionPoolListItem.spec.tsx @@ -0,0 +1,39 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { MINION_POOL_MOCK } from "@tests/mocks/MinionPoolMock"; + +import MinionPoolListItem from "./MinionPoolListItem"; + +describe("MinionPoolListItem", () => { + let defaultProps: MinionPoolListItem["props"]; + + beforeEach(() => { + defaultProps = { + item: MINION_POOL_MOCK, + selected: false, + onClick: jest.fn(), + endpointType: jest.fn(), + onSelectedChange: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(MINION_POOL_MOCK.name)).toBeTruthy(); + }); +}); diff --git a/src/components/modules/MinionModule/MinionPoolModal/MinionPoolModalContent.spec.tsx b/src/components/modules/MinionModule/MinionPoolModal/MinionPoolModalContent.spec.tsx new file mode 100644 index 00000000..df1678df --- /dev/null +++ b/src/components/modules/MinionModule/MinionPoolModal/MinionPoolModalContent.spec.tsx @@ -0,0 +1,113 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { fireEvent, render } from "@testing-library/react"; +import { OPENSTACK_ENDPOINT_MOCK } from "@tests/mocks/EndpointsMock"; +import TestUtils from "@tests/TestUtils"; + +import MinionPoolModalContent from "./MinionPoolModalContent"; + +jest.mock("@src/plugins/default/ContentPlugin", () => jest.fn(() => null)); + +jest.mock("@src/components/modules/EndpointModule/EndpointLogos", () => ({ + __esModule: true, + default: (props: any) =>
, +})); + +const DEFAULT_SCHEMA_MOCK = [ + { + name: "name", + type: "string", + required: true, + }, + { + name: "endpoint_id", + type: "string", + required: true, + }, + { + name: "platform", + type: "string", + required: true, + }, +]; + +const ENV_SCHEMA_MOCK = [ + { + name: "env_option", + type: "string", + }, + { + name: "required_env_option", + type: "string", + required: true, + }, +]; + +describe("MinionPoolModalContent", () => { + let defaultProps: MinionPoolModalContent["props"]; + + beforeEach(() => { + defaultProps = { + envOptionsDisabled: false, + defaultSchema: [...DEFAULT_SCHEMA_MOCK], + envSchema: [...ENV_SCHEMA_MOCK], + invalidFields: [ENV_SCHEMA_MOCK[1].name], + endpoint: OPENSTACK_ENDPOINT_MOCK, + platform: "source", + optionsLoading: false, + optionsLoadingSkipFields: [], + disabled: false, + cancelButtonText: "Cancel", + getFieldValue: jest.fn(), + onFieldChange: jest.fn(), + onResizeUpdate: jest.fn(), + scrollableRef: jest.fn(), + onCreateClick: jest.fn(), + onCancelClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Environment Options")).toBeTruthy(); + }); + + it("calls resize on simple / advanced toggle", () => { + const { getByText } = render(); + getByText("Advanced").click(); + expect(defaultProps.onResizeUpdate).toHaveBeenCalled(); + }); + + it("filters non required fields", () => { + const { getByText } = render(); + expect(TestUtils.selectAll("FieldInput__LabelText")[1].textContent).toBe( + "Required Env Option" + ); + getByText("Advanced").click(); + expect(TestUtils.selectAll("FieldInput__LabelText")[1].textContent).toBe( + "Env Option" + ); + }); + + it("fires onFieldChange", () => { + render(); + fireEvent.change(TestUtils.select("TextInput__Input")!, { + target: { value: "test" }, + }); + expect(defaultProps.onFieldChange).toHaveBeenCalled(); + }); +}); diff --git a/src/components/modules/NavigationModule/DetailsNavigation/test.tsx b/src/components/modules/NavigationModule/DetailsNavigation/test.tsx deleted file mode 100644 index 94a565be..00000000 --- a/src/components/modules/NavigationModule/DetailsNavigation/test.tsx +++ /dev/null @@ -1,55 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TestWrapper from "@src/utils/TestWrapper"; -import DetailsNavigation from "."; - -const wrap = props => - new TestWrapper( - shallow(), - "detailsNavigation" - ); -const items = [ - { label: "Item 1", value: "item-1" }, - { label: "Item 2", value: "item-2" }, - { label: "Item 3", value: "item-3" }, -]; - -describe("DetailsNavigation Component", () => { - // it('renders 3 items', () => { - // let wrapper = wrap({ items}) - // console.log(wrapper.find('dn-wrapper').debug()) - // // items.forEach(item => { - // // expect(wrapper.find(item.value).shallow.dive().dive()).toBe(item.label) - // // }) - // }) - - it("has items with correct href attribute", () => { - const wrapper = wrap({ items, itemType: "replica", itemId: "item-id" }); - expect(wrapper.find(items[0].value).prop("to")).toBe( - "/replica/item-1/item-id" - ); - }); - - it("has items with correct href attribute, if items have no value", () => { - const wrapper = wrap({ - items: [{ label: "Item 1", value: "" }], - itemType: "migration", - itemId: "item-id", - }); - expect(wrapper.find("").prop("to")).toBe("/migration/item-id"); - }); -}); diff --git a/src/components/modules/NavigationModule/Navigation/Navigation.tsx b/src/components/modules/NavigationModule/Navigation/Navigation.tsx index ca1c71c0..dc85271c 100644 --- a/src/components/modules/NavigationModule/Navigation/Navigation.tsx +++ b/src/components/modules/NavigationModule/Navigation/Navigation.tsx @@ -229,7 +229,7 @@ const CbsLogoSmall = styled.a` display: flex; transition: opacity ${ANIMATION}; `; -export const TEST_ID = "navigation"; + type Props = { currentPage?: string; className?: string; diff --git a/src/components/modules/NavigationModule/NavigationMini/NavigationMini.spec.tsx b/src/components/modules/NavigationModule/NavigationMini/NavigationMini.spec.tsx new file mode 100644 index 00000000..308b16c7 --- /dev/null +++ b/src/components/modules/NavigationModule/NavigationMini/NavigationMini.spec.tsx @@ -0,0 +1,42 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; + +import NavigationMini from "./"; +import TestUtils from "@tests/TestUtils"; + +jest.mock("react-router-dom", () => ({ Link: "a" })); +jest.mock("@src/components/modules/NavigationModule/Navigation", () => ({ + __esModule: true, + default: (props: any) => ( +
open: {String(props.open)}
+ ), +})); + +describe("NavigationMini", () => { + it("renders without crasing", () => { + render(); + expect(TestUtils.select("NavigationMini__Wrapper")).toBeTruthy(); + }); + + it("toggles the menu", () => { + const { getByTestId } = render(); + expect(getByTestId("navigation").textContent).toBe("open: false"); + TestUtils.select("NavigationMini__MenuImage")!.click(); + expect(getByTestId("navigation").textContent).toBe("open: true"); + }); +}); diff --git a/src/components/modules/NavigationModule/NavigationMini/NavigationMini.tsx b/src/components/modules/NavigationModule/NavigationMini/NavigationMini.tsx index 982974b3..8c0edaff 100644 --- a/src/components/modules/NavigationModule/NavigationMini/NavigationMini.tsx +++ b/src/components/modules/NavigationModule/NavigationMini/NavigationMini.tsx @@ -71,8 +71,6 @@ const NavigationStyled = styled(Navigation)` z-index: 9; `; -export const TEST_ID = "navigationMini"; - type State = { open: boolean; }; diff --git a/src/components/modules/ProjectModule/ProjectDetailsContent/ProjectDetailsContent.spec.tsx b/src/components/modules/ProjectModule/ProjectDetailsContent/ProjectDetailsContent.spec.tsx new file mode 100644 index 00000000..d3fe309c --- /dev/null +++ b/src/components/modules/ProjectModule/ProjectDetailsContent/ProjectDetailsContent.spec.tsx @@ -0,0 +1,197 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { Project, RoleAssignment } from "@src/@types/Project"; +import { render } from "@testing-library/react"; + +import ProjectDetailsContent from "./"; +import { User } from "@src/@types/User"; +import TestUtils from "@tests/TestUtils"; + +jest.mock("react-router-dom", () => ({ Link: "a" })); + +const PROJECT: Project = { + id: "project-id", + name: "project-name", +}; +const USER: User = { + id: "user-id", + name: "user-name", + project: PROJECT, + email: "user-email", + enabled: true, +}; +const ROLE_ASSIGNMENT: RoleAssignment = { + scope: { + project: PROJECT, + }, + role: { + id: "role-id", + name: "role-name", + }, + user: USER, +}; + +describe("ProjectDetailsContent", () => { + let defaultProps: ProjectDetailsContent["props"]; + + beforeEach(() => { + defaultProps = { + project: PROJECT, + loading: false, + users: [USER], + usersLoading: false, + roleAssignments: [ROLE_ASSIGNMENT], + roles: [ROLE_ASSIGNMENT.role], + loggedUserId: "admin", + onEnableUser: jest.fn(), + onRemoveUser: jest.fn(), + onUserRoleChange: jest.fn(), + onAddMemberClick: jest.fn(), + onDeleteClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(PROJECT.name)).toBeTruthy(); + expect(getByText(PROJECT.id)).toBeTruthy(); + }); + + describe("user actions", () => { + const openActionsDropdown = () => { + const dropdowns = TestUtils.selectAll("DropdownLink__LinkButton"); + let actionsDropdown; + + for (const dropdown of dropdowns) { + if (dropdown.textContent?.includes("Actions")) { + actionsDropdown = dropdown; + break; + } + } + + expect(actionsDropdown).toBeTruthy(); + actionsDropdown?.click(); + }; + + const clickAction = (action: string) => { + const items = TestUtils.selectAll("DropdownLink__ListItem-"); + let actionItem; + + for (const item of items) { + if (item.textContent?.includes(action)) { + actionItem = item; + break; + } + } + + expect(actionItem).toBeTruthy(); + actionItem?.click(); + }; + + it("removes user", () => { + render(); + + openActionsDropdown(); + clickAction("Remove"); + + expect(TestUtils.select("AlertModal__Message")).toBeTruthy(); + TestUtils.select("AlertModal__Buttons") + ?.querySelectorAll("button")[1] + .click(); + + expect(defaultProps.onRemoveUser).toBeCalled(); + }); + + it("cancels removing user", () => { + render(); + + openActionsDropdown(); + clickAction("Remove"); + + expect(TestUtils.select("AlertModal__Message")).toBeTruthy(); + TestUtils.select("AlertModal__Buttons") + ?.querySelectorAll("button")[0] + .click(); + + expect(defaultProps.onRemoveUser).not.toBeCalled(); + }); + + it("enables user", () => { + render( + + ); + + openActionsDropdown(); + clickAction("Enable"); + + expect(defaultProps.onEnableUser).toBeCalled(); + }); + + it("handles invalid action", () => { + const component = new ProjectDetailsContent(defaultProps); + component.handleUserAction(USER, { label: "Invalid", value: "invalid" }); + + expect(defaultProps.onEnableUser).not.toBeCalled(); + expect(defaultProps.onRemoveUser).not.toBeCalled(); + }); + }); + + it("renders loading", () => { + render(); + expect( + TestUtils.select("ProjectDetailsContent__LoadingWrapper") + ).toBeTruthy(); + }); + + it("changes user role", () => { + render(); + const dropdowns = TestUtils.selectAll("DropdownLink__LinkButton"); + let roleDropdown; + + for (const dropdown of dropdowns) { + if (dropdown.textContent?.includes(ROLE_ASSIGNMENT.role.name)) { + roleDropdown = dropdown; + break; + } + } + + expect(roleDropdown).toBeTruthy(); + roleDropdown?.click(); + + const items = TestUtils.selectAll("DropdownLink__ListItem-"); + let roleItem; + + for (const item of items) { + if (item.textContent?.includes(ROLE_ASSIGNMENT.role.name)) { + roleItem = item; + break; + } + } + + expect(roleItem).toBeTruthy(); + roleItem?.click(); + + expect(defaultProps.onUserRoleChange).toBeCalledWith( + USER, + ROLE_ASSIGNMENT.role.id, + false + ); + }); +}); diff --git a/src/components/modules/ProjectModule/ProjectDetailsContent/ProjectDetailsContent.tsx b/src/components/modules/ProjectModule/ProjectDetailsContent/ProjectDetailsContent.tsx index fced7f61..b4421931 100644 --- a/src/components/modules/ProjectModule/ProjectDetailsContent/ProjectDetailsContent.tsx +++ b/src/components/modules/ProjectModule/ProjectDetailsContent/ProjectDetailsContent.tsx @@ -12,22 +12,22 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +import { observer } from "mobx-react"; import React from "react"; import { Link } from "react-router-dom"; -import { observer } from "mobx-react"; import styled, { css } from "styled-components"; +import { ThemePalette, ThemeProps } from "@src/components/Theme"; import AlertModal from "@src/components/ui/AlertModal"; -import Table from "@src/components/ui/Table"; -import CopyValue from "@src/components/ui/CopyValue"; +import Button from "@src/components/ui/Button"; import CopyMultilineValue from "@src/components/ui/CopyMultilineValue"; -import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; +import CopyValue from "@src/components/ui/CopyValue"; import DropdownLink from "@src/components/ui/Dropdowns/DropdownLink"; -import Button from "@src/components/ui/Button"; +import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; +import Table from "@src/components/ui/Table"; import type { Project, RoleAssignment, Role } from "@src/@types/Project"; import type { User } from "@src/@types/User"; -import { ThemePalette, ThemeProps } from "@src/components/Theme"; const Wrapper = styled.div` ${ThemeProps.exactWidth(ThemeProps.contentWidth)} @@ -170,13 +170,7 @@ class ProjectDetailsContent extends React.Component { - diff --git a/src/components/modules/ProjectModule/ProjectDetailsContent/test.tsx b/src/components/modules/ProjectModule/ProjectDetailsContent/test.tsx deleted file mode 100644 index b8b1e0eb..00000000 --- a/src/components/modules/ProjectModule/ProjectDetailsContent/test.tsx +++ /dev/null @@ -1,83 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import type { Project, Role, RoleAssignment } from "@src/@types/Project"; -import type { User } from "@src/@types/User"; -import ProjectDetailsContent from "."; - -type Props = { - project: ?Project; - loading: boolean; - users: User[]; - usersLoading: boolean; - deleteDisabled: boolean; - roleAssignments: RoleAssignment[]; - roles: Role[]; - loggedUserId: string; -}; -const wrap = (props: Props) => - new TW( - shallow( - {}} - onDeleteClick={() => {}} - onEditProjectClick={() => {}} - onEnableUser={() => {}} - onRemoveUser={() => {}} - onUserRoleChange={() => {}} - {...props} - /> - ), - "pdContent" - ); -const projects: Project[] = [ - { id: "project-1", name: "Project 1" }, - { id: "project-2", name: "Project 2" }, -]; -const users: User[] = [ - { id: "user-1", name: "User 1", email: "email1", project: projects[0] }, - { id: "user-2", name: "User 2", email: "email2", project: projects[1] }, -]; -const roles: Role[] = [ - { id: "role-1", name: "Role 1" }, - { id: "role-2", name: "Role 2" }, -]; -const roleAssignments: RoleAssignment[] = [ - { user: users[0], role: roles[0], scope: { project: projects[0] } }, - { user: users[1], role: roles[1], scope: { project: projects[0] } }, -]; -describe("ProjectDetailsContent Component", () => { - it("renders info", () => { - const wrapper = wrap({ - project: projects[0], - loading: false, - users, - usersLoading: false, - deleteDisabled: false, - roleAssignments, - roles, - loggedUserId: "user-1", - }); - expect(wrapper.find("name").prop("value")).toBe("Project 1"); - expect(wrapper.find("id").prop("value")).toBe("project-1"); - const rows = wrapper.find("members").prop("items"); - expect(rows[0][1].props.selectedItems.length).toBe(1); - expect(rows[0][1].props.selectedItems[0]).toBe("role-1"); - expect(rows[1][1].props.selectedItems.length).toBe(1); - expect(rows[1][1].props.selectedItems[0]).toBe("role-2"); - }); -}); diff --git a/src/components/modules/ProjectModule/ProjectListItem/ProjectListItem.spec.tsx b/src/components/modules/ProjectModule/ProjectListItem/ProjectListItem.spec.tsx new file mode 100644 index 00000000..143e3acd --- /dev/null +++ b/src/components/modules/ProjectModule/ProjectListItem/ProjectListItem.spec.tsx @@ -0,0 +1,55 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { fireEvent, render } from "@testing-library/react"; + +import ProjectListItem from "."; + +describe("ProjectListItem", () => { + let defaultProps: ProjectListItem["props"]; + + beforeEach(() => { + defaultProps = { + item: { + id: "project-id", + name: "project-name", + }, + onClick: jest.fn(), + getMembers: jest.fn(), + isCurrentProject: jest.fn(), + onSwitchProjectClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(defaultProps.item.name)).toBeTruthy(); + }); + + it("switches project", () => { + render(); + const switchProjectButton = Array.from( + document.querySelectorAll("button") + ).find(el => el.textContent?.includes("Switch")); + expect(switchProjectButton).toBeTruthy(); + + fireEvent.mouseDown(switchProjectButton!); + fireEvent.mouseUp(switchProjectButton!); + + switchProjectButton!.click(); + expect(defaultProps.onSwitchProjectClick).toHaveBeenCalled(); + }); +}); diff --git a/src/components/modules/ProjectModule/ProjectListItem/test.tsx b/src/components/modules/ProjectModule/ProjectListItem/test.tsx deleted file mode 100644 index 65cbab15..00000000 --- a/src/components/modules/ProjectModule/ProjectListItem/test.tsx +++ /dev/null @@ -1,81 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import ProjectListItem from "."; -import type { Project } from "@src/@types/Project"; - -type Props = { - item: Project; - onClick: () => void; - getMembers: (projectId: string) => number; - isCurrentProject: (projectId: string) => boolean; - onSwitchProjectClick: (projectId: string) => void; -}; - -const wrap = (props: Props) => - new TW(shallow(), "plItem"); - -const item: Project = { - id: "p_id", - name: "p_name", - description: "p_description", - enabled: true, -}; -describe("ProjectListItem Component", () => { - it("renders with correct data", () => { - const wrapper = wrap({ - item, - onClick: () => {}, - getMembers: () => 3, - isCurrentProject: () => true, - onSwitchProjectClick: () => {}, - }); - expect(wrapper.findText("name")).toBe(item.name); - expect(wrapper.findText("description")).toBe(item.description); - expect(wrapper.findText("members")).toBe("3"); - expect(wrapper.findText("enabled")).toBe("Yes"); - expect(wrapper.findText("currentButton", false, true)).toBe("Current"); - }); - - it("dispatches click", () => { - const onClick = sinon.spy(); - const wrapper = wrap({ - item, - onClick, - getMembers: () => 3, - isCurrentProject: () => true, - onSwitchProjectClick: () => {}, - }); - wrapper.find("content").click(); - expect(onClick.calledOnce).toBe(true); - }); - - it("dispatches switch project click", () => { - const onSwitchProjectClick = sinon.spy(); - const wrapper = wrap({ - item, - onClick: () => {}, - getMembers: () => 3, - isCurrentProject: () => true, - onSwitchProjectClick, - }); - wrapper.find("currentButton").click(); - expect(onSwitchProjectClick.calledOnce).toBe(true); - expect(onSwitchProjectClick.args[0][0]).toBe("p_id"); - }); -}); diff --git a/src/components/modules/ProjectModule/ProjectMemberModal/test.tsx b/src/components/modules/ProjectModule/ProjectMemberModal/test.tsx deleted file mode 100644 index ffdeca6b..00000000 --- a/src/components/modules/ProjectModule/ProjectMemberModal/test.tsx +++ /dev/null @@ -1,151 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import type { User } from "@src/@types/User"; -import type { Project, Role } from "@src/@types/Project"; -import ProjectMemberModal from "."; - -type Props = { - loading: boolean; - users: User[]; - projects: Project[]; - onRequestClose: () => void; - onAddClick: (user: User, isNew: boolean, roles: Role[]) => void; - roles: Role[]; -}; -const wrap = (props: Props) => - new TW(shallow(), "pmModal"); -const users: User[] = [ - { id: "user-1", name: "User 1", email: "", project: { id: "", name: "" } }, - { id: "user-2", name: "User 2", email: "", project: { id: "", name: "" } }, -]; -const projects: Project[] = [ - { id: "project-1", name: "Project 1" }, - { id: "project-2", name: "Project 2" }, -]; -const roles: Role[] = [ - { id: "role-1", name: "Role 1" }, - { id: "role-2", name: "Role 2" }, - { id: "role-3", name: "Role 3" }, -]; -describe("ProjectMemberModal Component", () => { - it("renders existing user form", () => { - const wrapper = wrap({ - loading: false, - users, - projects, - roles, - onRequestClose: () => {}, - onAddClick: () => {}, - }); - expect(wrapper.find("users").prop("items")[1].value).toBe(users[1].id); - expect(wrapper.find("roles").prop("enum")[1].id).toBe(roles[1].id); - expect(wrapper.find("users").prop("highlight")).toBe(false); - expect(wrapper.find("roles").prop("highlight")).toBe(false); - expect(wrapper.find("users").prop("disabled")).toBe(false); - expect(wrapper.find("roles").prop("disabled")).toBe(false); - }); - - it("highlights required fields in existing user form", () => { - const wrapper = wrap({ - loading: false, - users, - projects, - roles, - onRequestClose: () => {}, - onAddClick: () => {}, - }); - expect(wrapper.find("users").length).toBe(1); - wrapper.find("addButton").click(); - expect(wrapper.find("users").prop("highlight")).toBe(true); - expect(wrapper.find("roles").prop("highlight")).toBe(true); - }); - - it("renders new user form and highlights required", () => { - const wrapper = wrap({ - loading: false, - users, - projects, - roles, - onRequestClose: () => {}, - onAddClick: () => {}, - }); - wrapper.find("formToggle").simulate("change", { value: "new" }); - expect(wrapper.find("users").length).toBe(0); - expect(wrapper.find("field-username").prop("highlight")).toBe(false); - expect(wrapper.find("field-description").prop("highlight")).toBe(false); - expect(wrapper.find("field-Primary Project").prop("highlight")).toBe(false); - expect(wrapper.find("roles").prop("highlight")).toBe(false); - expect(wrapper.find("field-password").prop("highlight")).toBe(false); - expect(wrapper.find("field-confirm_password").prop("highlight")).toBe( - false - ); - expect(wrapper.find("field-Email").prop("highlight")).toBe(false); - wrapper.find("addButton").click(); - expect(wrapper.find("field-username").prop("highlight")).toBe(true); - expect(wrapper.find("field-description").prop("highlight")).toBe(false); - expect(wrapper.find("field-Primary Project").prop("highlight")).toBe(false); - expect(wrapper.find("roles").prop("highlight")).toBe(true); - expect(wrapper.find("field-password").prop("highlight")).toBe(true); - expect(wrapper.find("field-confirm_password").prop("highlight")).toBe( - false - ); - expect(wrapper.find("field-Email").prop("highlight")).toBe(false); - }); - - it("dispatches add click with correct data", () => { - const onAddClick = sinon.spy(); - const wrapper = wrap({ - loading: false, - users, - projects, - roles, - onRequestClose: () => {}, - onAddClick, - }); - wrapper.find("formToggle").simulate("change", { value: "new" }); - wrapper.find("field-username").simulate("change", "new-username"); - wrapper.find("roles").simulate("change", "role-2"); - wrapper.find("roles").simulate("change", "role-1"); - wrapper.find("roles").simulate("change", "role-2"); - wrapper.find("roles").simulate("change", "role-3"); - wrapper.find("field-password").simulate("change", "new-password"); - wrapper.find("field-confirm_password").simulate("change", "new-password"); - wrapper.find("addButton").click(); - const userArg = onAddClick.args[0][0]; - const rolesArg: Role[] = onAddClick.args[0][2]; - expect(userArg.name).toBe("new-username"); - expect(userArg.password).toBe("new-password"); - expect(rolesArg.length).toBe(2); - expect(rolesArg[0].id).toBe("role-1"); - expect(rolesArg[1].id).toBe("role-3"); - }); - - it("disabled on loading", () => { - const wrapper = wrap({ - loading: true, - users, - projects, - roles, - onRequestClose: () => {}, - onAddClick: () => {}, - }); - expect(wrapper.find("users").prop("disabled")).toBe(true); - expect(wrapper.find("roles").prop("disabled")).toBe(true); - }); -}); diff --git a/src/components/modules/ProjectModule/ProjectModal/test.tsx b/src/components/modules/ProjectModule/ProjectModal/test.tsx deleted file mode 100644 index 64374ddf..00000000 --- a/src/components/modules/ProjectModule/ProjectModal/test.tsx +++ /dev/null @@ -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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import type { Project } from "@src/@types/Project"; -import ProjectModal from "."; - -type Props = { - project?: ?Project; - isNewProject?: boolean; - loading: boolean; - onRequestClose: () => void; - onUpdateClick: (project: Project) => void; -}; - -const wrap = (props: Props) => - new TW(shallow(), "projectModal"); - -describe("ProjectModal Component", () => { - it("doesn't dispatch click if project name is not filled", () => { - const onUpdateClick = sinon.spy(); - const wrapper = wrap({ - isNewProject: true, - loading: false, - onRequestClose: () => {}, - onUpdateClick, - }); - expect(wrapper.findText("updateButton", false, true)).toBe("New Project"); - wrapper.find("updateButton").click(); - expect(onUpdateClick.called).toBe(false); - expect(wrapper.find("field-project_name").prop("highlight")).toBe(true); - }); - - it("dispatches click if project is filled", () => { - const onUpdateClick = sinon.spy(); - const wrapper = wrap({ - isNewProject: false, - project: { id: "project", name: "Project Name" }, - loading: false, - onRequestClose: () => {}, - onUpdateClick, - }); - expect(wrapper.findText("updateButton", false, true)).toBe( - "Update Project" - ); - wrapper.find("updateButton").click(); - expect(onUpdateClick.called).toBe(true); - }); - - it("has disabled fields on loading", () => { - const wrapper = wrap({ - isNewProject: false, - project: { id: "project", name: "Project Name" }, - loading: true, - onRequestClose: () => {}, - onUpdateClick: () => {}, - }); - expect(wrapper.find("updateButton").prop("disabled")).toBe(true); - expect(wrapper.find("field-project_name").prop("disabled")).toBe(true); - }); -}); diff --git a/src/components/modules/SetupModule/SetupPageEmailBody/SetupPageEmailBody.spec.tsx b/src/components/modules/SetupModule/SetupPageEmailBody/SetupPageEmailBody.spec.tsx new file mode 100644 index 00000000..e433d7e8 --- /dev/null +++ b/src/components/modules/SetupModule/SetupPageEmailBody/SetupPageEmailBody.spec.tsx @@ -0,0 +1,83 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { CustomerInfoBasic, CustomerInfoTrial } from "@src/@types/InitialSetup"; +import DomUtils from "@src/utils/DomUtils"; +import { render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import SetupPageEmailBody from "./"; + +jest.mock("@src/utils/Config", () => ({ + config: { + providerSortPriority: {}, + providerNames: { + openstack: "OpenStack", + vmware_vsphere: "VMware vSphere", + }, + }, +})); + +jest.mock("@src/utils/DomUtils", () => ({ + copyTextToClipboard: jest.fn(), +})); + +const CUSTOMER_INFO_BASIC: CustomerInfoBasic = { + fullName: "John Doe", + email: "email@email.com", + company: "Company", + country: "Country", +}; + +const customerInfoTrial: CustomerInfoTrial = { + interestedIn: "migrations", + sourcePlatform: "vmware_vsphere", + destinationPlatform: "openstack", +}; + +describe("SetupPageEmailBody", () => { + let defaultProps: SetupPageEmailBody["props"]; + + beforeEach(() => { + defaultProps = { + customerInfoBasic: CUSTOMER_INFO_BASIC, + customerInfoTrial: customerInfoTrial, + licenceType: "trial", + applianceId: "appliance-id", + }; + }); + + it("renders without crashing", () => { + render(); + expect( + Array.from(document.querySelectorAll("*")).find(el => + el.textContent?.includes(CUSTOMER_INFO_BASIC.fullName) + ) + ).toBeTruthy(); + }); + + it("handles copy", () => { + render(); + TestUtils.select("CopyButton__Wrapper")!.click(); + expect(DomUtils.copyTextToClipboard).toHaveBeenCalled(); + }); + + it("copy is not called if no email template", () => { + const component = new SetupPageEmailBody(defaultProps); + component.handleCopy(); + expect(DomUtils.copyTextToClipboard).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/modules/SetupModule/SetupPageEmailBody/SetupPageEmailBody.tsx b/src/components/modules/SetupModule/SetupPageEmailBody/SetupPageEmailBody.tsx index 3ffaf207..4810aad8 100644 --- a/src/components/modules/SetupModule/SetupPageEmailBody/SetupPageEmailBody.tsx +++ b/src/components/modules/SetupModule/SetupPageEmailBody/SetupPageEmailBody.tsx @@ -12,20 +12,21 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import * as React from "react"; import { observer } from "mobx-react"; +import * as React from "react"; import styled from "styled-components"; + import { CustomerInfoBasic, CustomerInfoTrial, SetupPageLicenceType, } from "@src/@types/InitialSetup"; -import { customerInfoSetupStoreValueToString } from "@src/stores/SetupStore"; -import notificationStore from "@src/stores/NotificationStore"; -import { ThemePalette } from "@src/components/Theme"; -import SetupPageServerError from "@src/components/modules/SetupModule/ui/SetupPageServerError"; import SetupPageInputWrapper from "@src/components/modules/SetupModule/ui/SetupPageInputWrapper"; +import SetupPageServerError from "@src/components/modules/SetupModule/ui/SetupPageServerError"; +import { ThemePalette } from "@src/components/Theme"; import CopyButton from "@src/components/ui/CopyButton"; +import { customerInfoSetupStoreValueToString } from "@src/stores/SetupStore"; +import DomUtils from "@src/utils/DomUtils"; const Wrapper = styled.div``; const Link = styled.a` @@ -59,28 +60,18 @@ type Props = { class SetupPageEmailBody extends React.Component { emailTemplate: HTMLElement | null = null; - handleCopy(event?: React.ClipboardEvent) { + async handleCopy(event?: React.ClipboardEvent) { event?.preventDefault(); if (!this.emailTemplate) { return; } - try { - const range = document.createRange(); - range.selectNode(this.emailTemplate); - window.getSelection()?.removeAllRanges(); - window.getSelection()?.addRange(range); - document.execCommand("copy"); - if (!event) { - notificationStore.alert( - "The email body was succesfully copied to clipboard", - "success" - ); - } - } catch (err) { - notificationStore.alert("Error copying to clipboard", "error"); - } + const range = document.createRange(); + range.selectNode(this.emailTemplate); + window.getSelection()?.removeAllRanges(); + window.getSelection()?.addRange(range); + await DomUtils.copyTextToClipboard(window.getSelection()?.toString() || ""); } render() { diff --git a/src/components/modules/NavigationModule/Navigation/test.tsx b/src/components/modules/SetupModule/SetupPageHelp/SetupPageHelp.spec.tsx similarity index 53% rename from src/components/modules/NavigationModule/Navigation/test.tsx rename to src/components/modules/SetupModule/SetupPageHelp/SetupPageHelp.spec.tsx index 4f825648..838e5a0b 100644 --- a/src/components/modules/NavigationModule/Navigation/test.tsx +++ b/src/components/modules/SetupModule/SetupPageHelp/SetupPageHelp.spec.tsx @@ -1,5 +1,5 @@ /* -Copyright (C) 2017 Cloudbase Solutions SRL +Copyright (C) 2023 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,17 +13,14 @@ along with this program. If not, see . */ import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import Navigation from "."; -const wrap = props => new TW(shallow(), "navigation"); +import { render } from "@testing-library/react"; -describe("Navigation Component", () => { - it("selects the current page", () => { - const wrapper = wrap({ currentPage: "endpoints" }); - expect(wrapper.find("item-endpoints").prop("selected")).toBe(true); - expect(wrapper.find("item-replicas").prop("selected")).toBe(false); - expect(wrapper.find("item-migrations").prop("selected")).toBe(false); +import SetupPageHelp from "."; + +describe("SetupPageHelp", () => { + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Coriolis® Help")).toBeTruthy(); }); }); diff --git a/src/components/modules/SetupModule/SetupPageHelp/SetupPageHelp.tsx b/src/components/modules/SetupModule/SetupPageHelp/SetupPageHelp.tsx index f401f79b..fe0119b0 100644 --- a/src/components/modules/SetupModule/SetupPageHelp/SetupPageHelp.tsx +++ b/src/components/modules/SetupModule/SetupPageHelp/SetupPageHelp.tsx @@ -36,7 +36,7 @@ const OpenInNewIconWrapper = styled.div` transform: scale(0.6); `; type Props = { - style: React.CSSProperties; + style?: React.CSSProperties; }; @observer @@ -49,7 +49,7 @@ class SetupPageHelp extends React.Component { Click the link below to view the Coriolis® documentation. There you can find all the help you need to get you started.

- + Coriolis® Documentation . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import SetupPageLegal from "./"; + +jest.mock("@src/utils/Config", () => ({ + config: { + providerSortPriority: { + openstack: 2, + vmware_vsphere: 1, + }, + providerNames: { + openstack: "OpenStack", + vmware_vsphere: "VMware vSphere", + }, + }, +})); + +describe("SetupPageLegal", () => { + let defaultProps: SetupPageLegal["props"]; + + beforeEach(() => { + defaultProps = { + licenceType: "trial", + customerInfoTrial: { + interestedIn: "migrations", + sourcePlatform: "vmware_vsphere", + destinationPlatform: "openstack", + }, + onCustomerInfoChange: jest.fn(), + onLegalChange: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Coriolis® Trial License")).toBeTruthy(); + }); + + it("fires interestedIn change event", () => { + const { rerender } = render(); + const findInputByLabel = (label: string) => + Array.from(document.querySelectorAll("label")) + .find(el => el.textContent?.includes(label))! + .querySelector("input")!; + + const replicasInput = findInputByLabel("Replicas"); + expect(replicasInput).toBeTruthy(); + replicasInput.click(); + + expect(defaultProps.onCustomerInfoChange).toHaveBeenCalledWith( + "interestedIn", + "replicas" + ); + + const bothInput = findInputByLabel("Both"); + expect(bothInput).toBeTruthy(); + bothInput.click(); + + expect(defaultProps.onCustomerInfoChange).toHaveBeenCalledWith( + "interestedIn", + "both" + ); + + rerender( + + ); + + const migrationsInput = findInputByLabel("Migrations"); + expect(migrationsInput).toBeTruthy(); + migrationsInput.click(); + + expect(defaultProps.onCustomerInfoChange).toHaveBeenCalledWith( + "interestedIn", + "migrations" + ); + }); + + it("fires legal agreement change event", () => { + render(); + const findCheckboxByText = (text: string) => + Array.from(TestUtils.selectAll("Checkbox__Wrapper")).find(el => + el.parentElement?.textContent?.includes(text) + )!; + + const privacyCheckbox = findCheckboxByText("Privacy Policy"); + expect(privacyCheckbox).toBeTruthy(); + + privacyCheckbox.click(); + expect(defaultProps.onLegalChange).toHaveBeenCalledWith(false); + + const eulaCheckbox = findCheckboxByText("EULA"); + expect(eulaCheckbox).toBeTruthy(); + + eulaCheckbox.click(); + expect(defaultProps.onLegalChange).toHaveBeenCalledWith(true); + + const findLabelByText = (text: string) => + Array.from(TestUtils.selectAll("SetupPageLegal__CheckboxLabel")).find( + el => el.textContent?.includes(text) + )!; + + const privacyLabel = findLabelByText("Privacy Policy"); + expect(privacyLabel).toBeTruthy(); + privacyLabel.click(); + expect(defaultProps.onLegalChange).toHaveBeenCalledWith(false); + + const eulaLabel = findLabelByText("EULA"); + expect(eulaLabel).toBeTruthy(); + eulaLabel.click(); + expect(defaultProps.onLegalChange).toHaveBeenCalledWith(false); + }); + + it.each` + platformType | itemIndex | expectedProvider + ${"Source"} | ${2} | ${"openstack"} + ${"Destination"} | ${3} | ${"aws"} + `( + "fires $platformType platform change event", + ({ platformType, itemIndex, expectedProvider }) => { + render(); + const platformDropdown = Array.from( + TestUtils.selectAll("Dropdown__Wrapper") + ).find(el => + el.parentElement?.parentElement?.textContent?.includes( + `${platformType} Platform` + ) + )!; + + expect(platformDropdown).toBeTruthy(); + TestUtils.select("DropdownButton__Wrapper", platformDropdown)!.click(); + TestUtils.selectAll("Dropdown__ListItem-")[itemIndex].click(); + + expect(defaultProps.onCustomerInfoChange).toHaveBeenCalledWith( + `${platformType.toLowerCase()}Platform`, + expectedProvider + ); + } + ); +}); diff --git a/src/components/modules/SetupModule/SetupPageLicence/SetupPageLicence.spec.tsx b/src/components/modules/SetupModule/SetupPageLicence/SetupPageLicence.spec.tsx new file mode 100644 index 00000000..f3874ebf --- /dev/null +++ b/src/components/modules/SetupModule/SetupPageLicence/SetupPageLicence.spec.tsx @@ -0,0 +1,99 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { fireEvent, render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import SetupPageLicence from "./"; + +describe("SetupPageLicence", () => { + let defaultProps: SetupPageLicence["props"]; + + beforeEach(() => { + defaultProps = { + customerInfo: { + fullName: "John Doe", + email: "email@email.com", + company: "Company", + country: "RO", + }, + highlightEmail: false, + highlightEmptyFields: false, + licenceType: "trial", + onUpdateCustomerInfo: jest.fn(), + onSubmit: jest.fn(), + onLicenceTypeChange: jest.fn(), + }; + }); + + it("renders without crashing", () => { + render(); + const fullNameInput = Array.from( + TestUtils.selectAll("SetupPageInputWrapper__Label") + ) + .find(el => el.textContent?.includes("Full name"))! + .parentElement?.querySelector("input")!; + + expect(fullNameInput).toBeTruthy(); + expect(fullNameInput.value).toBe("John Doe"); + }); + + it("submits form", () => { + render(); + fireEvent.submit(document.querySelector("form")!); + + expect(defaultProps.onSubmit).toHaveBeenCalled(); + }); + + it.each` + label | fieldName | newValue + ${"Full name"} | ${"fullName"} | ${"New Name"} + ${"Email"} | ${"email"} | ${"new@email.com"} + ${"Company"} | ${"company"} | ${"New Company"} + `("fires $label change event", ({ label, fieldName, newValue }) => { + render(); + const findInputByLabel = (label: string) => + Array.from(TestUtils.selectAll("SetupPageInputWrapper__Label")) + .find(el => el.textContent?.includes(label))! + .parentElement!.querySelector("input")!; + + const input = findInputByLabel(label); + fireEvent.change(input, { target: { value: newValue } }); + + expect(defaultProps.onUpdateCustomerInfo).toHaveBeenCalledWith( + fieldName, + newValue + ); + }); + + it("fires country change event", async () => { + render(); + const countryInput = Array.from( + TestUtils.selectAll("SetupPageInputWrapper__Label") + ) + .find(el => el.textContent?.includes("Country"))! + .parentElement?.querySelector("input")!; + + fireEvent.change(countryInput, { target: { value: "Unite" } }); + + fireEvent.click(TestUtils.selectAll("AutocompleteDropdown__ListItem-")[1]); + + expect(defaultProps.onUpdateCustomerInfo).toHaveBeenCalledWith( + "country", + "United Arab Emirates" + ); + }); +}); diff --git a/src/components/modules/WizardModule/WizardPageContent/test.tsx b/src/components/modules/SetupModule/SetupPageModuleWrapper/SetupPageModuleWrapper.spec.tsx similarity index 50% rename from src/components/modules/WizardModule/WizardPageContent/test.tsx rename to src/components/modules/SetupModule/SetupPageModuleWrapper/SetupPageModuleWrapper.spec.tsx index b34ba4f7..d408db44 100644 --- a/src/components/modules/WizardModule/WizardPageContent/test.tsx +++ b/src/components/modules/SetupModule/SetupPageModuleWrapper/SetupPageModuleWrapper.spec.tsx @@ -1,5 +1,5 @@ /* -Copyright (C) 2017 Cloudbase Solutions SRL +Copyright (C) 2023 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,24 @@ along with this program. If not, see . */ import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import WizardPageContent from "."; -const wrap = (props: any) => - new TW( - shallow( - {}} {...props} /> - ), - "wpContent" - ); +import { render } from "@testing-library/react"; -describe("WizardPageContent Component", () => { - it("renders wizard type page", () => { - const wrapper = wrap({ - page: { id: "type", title: "Wizard Type" }, - type: "replica", - }); - expect(wrapper.findText("header")).toBe("Wizard Type Replica"); - expect(wrapper.shallow.find("WizardType").prop("selected")).toBe("replica"); +import SetupPageModuleWrapper from "."; + +describe("SetupPageModuleWrapper", () => { + let defaultProps: SetupPageModuleWrapper["props"]; + + beforeEach(() => { + defaultProps = { + children:
children
, + actions:
actions
, + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("children")).toBeTruthy(); + expect(getByText("actions")).toBeTruthy(); }); }); diff --git a/src/components/modules/NavigationModule/NavigationMini/test.tsx b/src/components/modules/SetupModule/SetupPageWelcome/SetupPageWelcome.spec.tsx similarity index 56% rename from src/components/modules/NavigationModule/NavigationMini/test.tsx rename to src/components/modules/SetupModule/SetupPageWelcome/SetupPageWelcome.spec.tsx index 6e5ad4ec..6e56054d 100644 --- a/src/components/modules/NavigationModule/NavigationMini/test.tsx +++ b/src/components/modules/SetupModule/SetupPageWelcome/SetupPageWelcome.spec.tsx @@ -1,5 +1,5 @@ /* -Copyright (C) 2017 Cloudbase Solutions SRL +Copyright (C) 2023 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,19 +13,21 @@ along with this program. If not, see . */ import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import NavigationMini, { TEST_ID } from "."; +import { render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; -const wrap = () => new TW(shallow(), TEST_ID); +import SetupPageWelcome from "./"; -describe("NavigationMini Component", () => { - it("toggles the navigation state", () => { - const wrapper = wrap(); - const button = () => wrapper.find("toggleButton"); - expect(button().prop("open")).toBe(false); - button().simulate("click"); - expect(button().prop("open")).toBe(true); +describe("SetupPageWelcome", () => { + let defaultProps: SetupPageWelcome["props"]; + + beforeEach(() => { + defaultProps = {}; + }); + + it("renders without crashing", () => { + render(); + expect(TestUtils.select("SetupPageWelcome__Wrapper")).toBeTruthy(); }); }); diff --git a/src/components/modules/SetupModule/ui/SetupPageBackButton/SetupPageBackButton.spec.tsx b/src/components/modules/SetupModule/ui/SetupPageBackButton/SetupPageBackButton.spec.tsx new file mode 100644 index 00000000..b5f8202a --- /dev/null +++ b/src/components/modules/SetupModule/ui/SetupPageBackButton/SetupPageBackButton.spec.tsx @@ -0,0 +1,37 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import SetupPageBackButton from "./"; + +describe("SetupPageBackButton", () => { + let defaultProps: SetupPageBackButton["props"]; + + beforeEach(() => { + defaultProps = { + onClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + render(); + expect( + TestUtils.select("SetupPageBackButton__Wrapper")?.textContent + ).toContain("Back"); + }); +}); diff --git a/src/components/modules/SetupModule/ui/SetupPagePasswordStrength/SetupPagePasswordStrength.spec.tsx b/src/components/modules/SetupModule/ui/SetupPagePasswordStrength/SetupPagePasswordStrength.spec.tsx new file mode 100644 index 00000000..9a70eab2 --- /dev/null +++ b/src/components/modules/SetupModule/ui/SetupPagePasswordStrength/SetupPagePasswordStrength.spec.tsx @@ -0,0 +1,50 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { ThemePalette } from "@src/components/Theme"; +import { render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import SetupPagePasswordStrength from "./"; + +describe("SetupPagePasswordStrength", () => { + let defaultProps: SetupPagePasswordStrength["props"]; + + beforeEach(() => { + defaultProps = { + value: "password", + }; + }); + + it("renders without crashing", () => { + render(); + expect(TestUtils.select("SetupPagePasswordStrength__Wrapper")).toBeTruthy(); + }); + + it.each` + password | status | color + ${"a"} | ${"VERY_WEAK"} | ${ThemePalette.alert} + ${"A###d1!"} | ${"WEAK"} | ${ThemePalette.alert} + ${"A###$d123!"} | ${"REASONABLE"} | ${ThemePalette.warning} + ${"AmweyQe$d123!"} | ${"STRONG"} | ${"#758400"} + ${"AmwueyQe$d123!"} | ${"VERY_STRONG"} | ${"green"} + `("renders $color for $status password: $password", ({ password, color }) => { + render(); + const bar = TestUtils.select("SetupPagePasswordStrength__Bar")!; + const background = window.getComputedStyle(bar).background; + expect(TestUtils.rgbToHex(background)).toBe(color); + }); +}); diff --git a/src/components/modules/TemplateModule/DetailsTemplate/DetailsTemplate.spec.tsx b/src/components/modules/TemplateModule/DetailsTemplate/DetailsTemplate.spec.tsx new file mode 100644 index 00000000..aff7e44c --- /dev/null +++ b/src/components/modules/TemplateModule/DetailsTemplate/DetailsTemplate.spec.tsx @@ -0,0 +1,38 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; + +import DetailsTemplate, { Props } from "."; + +describe("DetailsTemplate", () => { + let defaultProps: Props; + + beforeEach(() => { + defaultProps = { + pageHeaderComponent:
pageHeaderComponent
, + contentHeaderComponent:
contentHeaderComponent
, + contentComponent:
contentComponent
, + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("pageHeaderComponent")).toBeTruthy(); + expect(getByText("contentHeaderComponent")).toBeTruthy(); + expect(getByText("contentComponent")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/TemplateModule/DetailsTemplate/DetailsTemplate.tsx b/src/components/modules/TemplateModule/DetailsTemplate/DetailsTemplate.tsx index a2baaae5..31f55d85 100644 --- a/src/components/modules/TemplateModule/DetailsTemplate/DetailsTemplate.tsx +++ b/src/components/modules/TemplateModule/DetailsTemplate/DetailsTemplate.tsx @@ -27,7 +27,7 @@ const Content = styled.div` flex-direction: column; min-height: 0; `; -type Props = { +export type Props = { pageHeaderComponent: React.ReactNode; contentHeaderComponent: React.ReactNode; contentComponent: React.ReactNode; diff --git a/src/components/modules/TemplateModule/EmptyTemplate/EmptyTemplate.spec.tsx b/src/components/modules/TemplateModule/EmptyTemplate/EmptyTemplate.spec.tsx new file mode 100644 index 00000000..f2008a4f --- /dev/null +++ b/src/components/modules/TemplateModule/EmptyTemplate/EmptyTemplate.spec.tsx @@ -0,0 +1,30 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; + +import EmptyTemplate from "."; + +describe("EmptyTemplate", () => { + it("renders without crashing", () => { + const { getByText } = render( + +
contentComponent
+
+ ); + expect(getByText("contentComponent")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/TemplateModule/MainTemplate/MainTemplate.spec.tsx b/src/components/modules/TemplateModule/MainTemplate/MainTemplate.spec.tsx new file mode 100644 index 00000000..9b3bc803 --- /dev/null +++ b/src/components/modules/TemplateModule/MainTemplate/MainTemplate.spec.tsx @@ -0,0 +1,34 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; + +import MainTemplate from "."; + +describe("MainTemplate", () => { + it("renders without crashing", () => { + const { getByText } = render( + navigationComponent
} + headerComponent={
headerComponent
} + listComponent={
listComponent
} + /> + ); + expect(getByText("navigationComponent")).toBeTruthy(); + expect(getByText("headerComponent")).toBeTruthy(); + expect(getByText("listComponent")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/TemplateModule/WizardTemplate/WizardTemplate.spec.tsx b/src/components/modules/TemplateModule/WizardTemplate/WizardTemplate.spec.tsx new file mode 100644 index 00000000..42d78693 --- /dev/null +++ b/src/components/modules/TemplateModule/WizardTemplate/WizardTemplate.spec.tsx @@ -0,0 +1,32 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; + +import WizardTemplate from "."; + +describe("WizardTemplate", () => { + it("renders without crashing", () => { + const { getByText } = render( + pageHeaderComponent} + pageContentComponent={
pageContentComponent
} + /> + ); + expect(getByText("pageHeaderComponent")).toBeTruthy(); + expect(getByText("pageContentComponent")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/TransferModule/DeleteReplicaModal/DeleteReplicaModal.spec.tsx b/src/components/modules/TransferModule/DeleteReplicaModal/DeleteReplicaModal.spec.tsx new file mode 100644 index 00000000..eb5348de --- /dev/null +++ b/src/components/modules/TransferModule/DeleteReplicaModal/DeleteReplicaModal.spec.tsx @@ -0,0 +1,59 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import DeleteReplicaModal from "./"; + +describe("DeleteReplicaModal", () => { + let defaultProps: DeleteReplicaModal["props"]; + + beforeEach(() => { + defaultProps = { + hasDisks: false, + onDeleteReplica: jest.fn(), + onDeleteDisks: jest.fn(), + onRequestClose: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Delete Replica")).toBeTruthy(); + }); + + it("renders with disks", () => { + render(); + expect( + TestUtils.select("DeleteReplicaModal__ExtraMessage")?.textContent + ).toContain("has been executed at least once"); + }); + + it("is multiple replica selection with disks", () => { + render( + + ); + expect( + TestUtils.select("DeleteReplicaModal__ExtraMessage")?.textContent + ).toContain("have been executed at least once"); + }); + + it("renders loading", () => { + render(); + expect(TestUtils.select("DeleteReplicaModal__Loading")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/TransferModule/DeleteReplicaModal/DeleteReplicaModal.tsx b/src/components/modules/TransferModule/DeleteReplicaModal/DeleteReplicaModal.tsx index d19fcd53..fa085c0b 100644 --- a/src/components/modules/TransferModule/DeleteReplicaModal/DeleteReplicaModal.tsx +++ b/src/components/modules/TransferModule/DeleteReplicaModal/DeleteReplicaModal.tsx @@ -12,16 +12,15 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled from "styled-components"; -import Modal from "@src/components/ui/Modal"; +import { ThemePalette } from "@src/components/Theme"; import Button from "@src/components/ui/Button"; +import Modal from "@src/components/ui/Modal"; import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; -import { ThemePalette } from "@src/components/Theme"; - const Wrapper = styled.div` display: flex; flex-direction: column; diff --git a/src/components/modules/TransferModule/Executions/Executions.spec.tsx b/src/components/modules/TransferModule/Executions/Executions.spec.tsx new file mode 100644 index 00000000..93b1f11d --- /dev/null +++ b/src/components/modules/TransferModule/Executions/Executions.spec.tsx @@ -0,0 +1,230 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { + EXECUTION_MOCK, + EXECUTION_TASKS_MOCK, +} from "@tests/mocks/ExecutionsMock"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; +import TestUtils from "@tests/TestUtils"; + +import Executions from "./"; + +describe("Executions", () => { + let defaultProps: Executions["props"]; + + beforeEach(() => { + defaultProps = { + executions: [EXECUTION_MOCK], + executionsTasks: [EXECUTION_TASKS_MOCK], + loading: false, + tasksLoading: false, + instancesDetails: [INSTANCE_MOCK], + onChange: jest.fn(), + onCancelExecutionClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(EXECUTION_TASKS_MOCK.tasks[0].id)).toBeTruthy(); + expect(getByText(EXECUTION_MOCK.id)).toBeTruthy(); + }); + + it("sets selected execution on new props", () => { + const { getByText, rerender } = render(); + rerender( + + ); + expect(getByText("new-id")).toBeTruthy(); + + rerender( + + ); + expect(getByText("new-id-2")).toBeTruthy(); + + rerender(); + expect(getByText(EXECUTION_MOCK.id)).toBeTruthy(); + }); + + it("renders with no executions", () => { + const { getByText, rerender } = render(); + expect(getByText(EXECUTION_MOCK.id)).toBeTruthy(); + + rerender(); + expect(getByText("This replica has not been executed yet.")).toBeTruthy(); + }); + + it("doesn't dispatch onChange if no executions", () => { + const { rerender } = render( + + ); + rerender(); + expect(defaultProps.onChange).not.toHaveBeenCalled(); + }); + + it("handles previous executions", () => { + const { rerender } = render( + + ); + const previousArrow = () => + TestUtils.selectAll( + "Arrow__Wrapper", + TestUtils.select("Timeline__Wrapper")! + )[0]; + previousArrow().click(); + expect(defaultProps.onChange).toHaveBeenLastCalledWith(EXECUTION_MOCK.id); + + rerender(); + previousArrow().click(); + expect(defaultProps.onChange).toHaveBeenLastCalledWith(EXECUTION_MOCK.id); + }); + + it("doesn't handle previous executions in edge cases", () => { + const executionsComponent = new Executions(defaultProps); + executionsComponent.handlePreviousExecutionClick(); + expect(defaultProps.onChange).not.toHaveBeenCalled(); + }); + + it("handles next executions", () => { + render( + + ); + const nextArrow = TestUtils.selectAll( + "Arrow__Wrapper", + TestUtils.select("Timeline__Wrapper")! + )[1]; + nextArrow.click(); + expect(defaultProps.onChange).toHaveBeenLastCalledWith("new-id"); + + const previousArrow = () => + TestUtils.selectAll( + "Arrow__Wrapper", + TestUtils.select("Timeline__Wrapper")! + )[0]; + previousArrow().click(); + + nextArrow.click(); + expect(defaultProps.onChange).toHaveBeenLastCalledWith("new-id"); + }); + + it("doesn't handle next executions in edge cases", () => { + const executionsComponent = new Executions(defaultProps); + executionsComponent.handleNextExecutionClick(); + expect(defaultProps.onChange).not.toHaveBeenCalled(); + }); + + it("handles timeline item click", () => { + render( + + ); + const timelineItem = TestUtils.select("Timeline__Item-"); + expect(timelineItem).toBeTruthy(); + timelineItem!.click(); + expect(defaultProps.onChange).toHaveBeenLastCalledWith(EXECUTION_MOCK.id); + }); + + it("handles cancel execution click", () => { + const newExecution = { ...EXECUTION_MOCK, id: "new-id", status: "RUNNING" }; + render( + + ); + const cancelExecutionButton = Array.from( + document.querySelectorAll("button") + ).find(el => el.textContent === "Cancel Execution"); + expect(cancelExecutionButton).toBeTruthy(); + cancelExecutionButton!.click(); + expect(defaultProps.onCancelExecutionClick).toHaveBeenCalledWith( + newExecution + ); + }); + + it("force cancels execution", () => { + const newExecution = { + ...EXECUTION_MOCK, + id: "new-id", + status: "CANCELLING", + }; + render( + + ); + const cancelExecutionButton = Array.from( + document.querySelectorAll("button") + ).find(el => el.textContent === "Force Cancel Execution"); + expect(cancelExecutionButton).toBeTruthy(); + cancelExecutionButton!.click(); + expect(defaultProps.onCancelExecutionClick).toHaveBeenCalledWith( + newExecution, + true + ); + }); + + it("renders loading", () => { + render(); + expect(TestUtils.select("Executions__LoadingWrapper")).toBeTruthy(); + }); + + it("deletes execution", () => { + const deleteExecution = jest.fn(); + render( + + ); + const deleteExecutionButton = Array.from( + document.querySelectorAll("button") + ).find(el => el.textContent === "Delete Execution"); + expect(deleteExecutionButton).toBeTruthy(); + deleteExecutionButton!.click(); + expect(deleteExecution).toHaveBeenCalledWith(EXECUTION_MOCK); + }); +}); diff --git a/src/components/modules/TransferModule/Executions/Executions.tsx b/src/components/modules/TransferModule/Executions/Executions.tsx index 83040df3..9583a64a 100644 --- a/src/components/modules/TransferModule/Executions/Executions.tsx +++ b/src/components/modules/TransferModule/Executions/Executions.tsx @@ -129,16 +129,12 @@ class Executions extends React.Component { if (this.props.executions.length > props.executions.length) { const isSelectedAvailable = props.executions.find( - e => - this.state.selectedExecution && - e.id === this.state.selectedExecution.id + e => e.id === this.state.selectedExecution?.id ); if (!isSelectedAvailable) { const lastIndex = this.props.executions ? this.props.executions.findIndex( - e => - this.state.selectedExecution && - e.id === this.state.selectedExecution.id + e => e.id === this.state.selectedExecution?.id ) : -1; if (props.executions.length) { diff --git a/src/components/modules/TransferModule/Executions/test.tsx b/src/components/modules/TransferModule/Executions/test.tsx deleted file mode 100644 index a42e981f..00000000 --- a/src/components/modules/TransferModule/Executions/test.tsx +++ /dev/null @@ -1,111 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import Executions from "."; - -const wrap = props => new TW(shallow(), "executions"); - -const item = { - executions: [ - { id: "execution-1", number: 1, status: "ERROR", created_at: new Date() }, - { - id: "execution-2", - number: 2, - status: "COMPLETED", - created_at: new Date(), - }, - { - id: "execution-3", - number: 3, - status: "CANCELED", - created_at: new Date(), - }, - { id: "execution-4", number: 4, status: "RUNNING", created_at: new Date() }, - ], -}; - -describe("Executions Component", () => { - it("selects last execution by default", () => { - const wrapper = wrap({ item }); - expect(wrapper.findText("number")).toBe("Execution #4"); - }); - - it("selects previous execution on previous click", () => { - const wrapper = wrap({ item }); - wrapper.find("timeline").simulate("previousClick"); - expect(wrapper.findText("number")).toBe("Execution #3"); - wrapper.find("timeline").simulate("previousClick"); - expect(wrapper.findText("number")).toBe("Execution #2"); - }); - - it("selects next execution on next click", () => { - const wrapper = wrap({ item }); - wrapper.find("timeline").simulate("previousClick"); - wrapper.find("timeline").simulate("previousClick"); - wrapper.find("timeline").simulate("nextClick"); - expect(wrapper.findText("number")).toBe("Execution #3"); - }); - - it("doesn't select next execution on next click if not possible", () => { - const wrapper = wrap({ item }); - wrapper.find("timeline").simulate("nextClick"); - expect(wrapper.findText("number")).toBe("Execution #4"); - }); - - it("shows cancel button on running executions", () => { - const wrapper = wrap({ item }); - expect(wrapper.find("cancelButton").length).toBe(1); - expect(wrapper.find("deleteButton").length).toBe(0); - }); - - it("shows delete button on non-running executions", () => { - const wrapper = wrap({ item }); - wrapper.find("timeline").simulate("previousClick"); - expect(wrapper.find("cancelButton").length).toBe(0); - expect(wrapper.find("deleteButton").length).toBe(1); - }); - - it("dispatches cancel click", () => { - const onCancelExecutionClick = sinon.spy(); - const wrapper = wrap({ item, onCancelExecutionClick }); - wrapper.find("cancelButton").simulate("click"); - expect(onCancelExecutionClick.calledOnce).toBe(true); - }); - - it("dispatches delete click", () => { - const onDeleteExecutionClick = sinon.spy(); - const wrapper = wrap({ item, onDeleteExecutionClick }); - wrapper.find("timeline").simulate("previousClick"); - wrapper.find("deleteButton").simulate("click"); - expect(onDeleteExecutionClick.calledOnce).toBe(true); - }); - - it("renders no executions", () => { - const wrapper = wrap({ item: {} }); - expect(wrapper.findText("noExTitle")).toBe( - "It looks like there are no executions in this replica." - ); - }); - - it("dispatches execute click", () => { - const onExecuteClick = sinon.spy(); - const wrapper = wrap({ item: {}, onExecuteClick }); - wrapper.find("executeButton").simulate("click"); - expect(onExecuteClick.calledOnce).toBe(true); - }); -}); diff --git a/src/components/modules/TransferModule/MainDetails/MainDetails.spec.tsx b/src/components/modules/TransferModule/MainDetails/MainDetails.spec.tsx new file mode 100644 index 00000000..5e61d724 --- /dev/null +++ b/src/components/modules/TransferModule/MainDetails/MainDetails.spec.tsx @@ -0,0 +1,105 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { + OPENSTACK_ENDPOINT_MOCK, + VMWARE_ENDPOINT_MOCK, +} from "@tests/mocks/EndpointsMock"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; +import { MINION_POOL_MOCK } from "@tests/mocks/MinionPoolMock"; +import { STORAGE_BACKEND_MOCK } from "@tests/mocks/StoragesMock"; +import { REPLICA_MOCK } from "@tests/mocks/TransferMock"; +import TestUtils from "@tests/TestUtils"; + +import MainDetails from "./"; + +jest.mock("@src/components/modules/EndpointModule/EndpointLogos", () => ({ + __esModule: true, + default: (props: any) =>
{props.endpoint}
, +})); +jest.mock("react-router-dom", () => ({ Link: "a" })); + +describe("MainDetails", () => { + let defaultProps: MainDetails["props"]; + + beforeEach(() => { + defaultProps = { + item: REPLICA_MOCK, + minionPools: [MINION_POOL_MOCK], + storageBackends: [STORAGE_BACKEND_MOCK], + destinationSchema: [], + destinationSchemaLoading: false, + sourceSchema: [], + sourceSchemaLoading: false, + instancesDetails: [INSTANCE_MOCK], + instancesDetailsLoading: false, + endpoints: [OPENSTACK_ENDPOINT_MOCK, VMWARE_ENDPOINT_MOCK], + bottomControls:
Bottom controls
, + loading: false, + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(REPLICA_MOCK.id)).toBeTruthy(); + expect(getByText("Bottom controls")).toBeTruthy(); + }); + + it("renders missing endpoint", () => { + const { getByText } = render( + + ); + expect(getByText("Endpoint is missing")).toBeTruthy(); + }); + + it("renders loading", () => { + render(); + expect(TestUtils.select("MainDetails__Loading")).toBeTruthy(); + }); + + it("renders allocating minions error", () => { + render( + + ); + expect( + Array.from(document.querySelectorAll("*")).find(el => + el.textContent?.includes("error allocating minion machines") + ) + ).toBeTruthy(); + }); + + it("shows password", () => { + const { getByText } = render(); + const passwordEl = TestUtils.select("PasswordValue__Wrapper")!; + expect(passwordEl).toBeTruthy(); + expect(passwordEl.textContent).toBe("•••••••••"); + + passwordEl.click(); + expect( + getByText(REPLICA_MOCK.destination_environment.password) + ).toBeTruthy(); + }); +}); diff --git a/src/components/modules/TransferModule/MainDetails/test.tsx b/src/components/modules/TransferModule/MainDetails/test.tsx deleted file mode 100644 index e6c624f8..00000000 --- a/src/components/modules/TransferModule/MainDetails/test.tsx +++ /dev/null @@ -1,80 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import moment from "moment"; -import TW from "@src/utils/TestWrapper"; -import MainDetails from "."; - -const wrap = props => - new TW(shallow(), "mainDetails"); - -const endpoints = [ - { id: "endpoint-1", name: "Endpoint OPS", type: "openstack" }, - { id: "endpoint-2", name: "Endpoint AZURE", type: "azure" }, -]; -const item = { - origin_endpoint_id: "endpoint-1", - destination_endpoint_id: "endpoint-2", - id: "item-id", - created_at: new Date(2017, 10, 24, 16, 15), - instances: ["instance_1"], - type: "Replica", - notes: "A description", -}; -const instancesDetails = [ - { - instance_name: "instance_1", - devices: { nics: [{ network_name: "network_1" }] }, - }, -]; - -describe("MainDetails Component", () => { - it("renders with endpoint missing", () => { - const wrapper = wrap({ item: {}, endpoints: [] }); - expect(wrapper.findText("missing-source")).toBe( - "Endpoint is missing" - ); - expect(wrapper.findText("missing-target")).toBe( - "Endpoint is missing" - ); - }); - - it("renders endpoint info", () => { - const wrapper = wrap({ item, endpoints, instancesDetails }); - expect(wrapper.find("id").prop("value")).toBe("item-id"); - const localDate = moment(item.created_at).add( - -new Date().getTimezoneOffset(), - "minutes" - ); - expect(wrapper.find("created").prop("value")).toBe( - localDate.format("YYYY-MM-DD HH:mm:ss") - ); - // expect(wrapper.find('name-source').shallow.dive().dive().text()).toBe('Endpoint OPS') - // expect(wrapper.findText('name-target')).toBe('Endpoint AZURE') - expect(wrapper.find("description").prop("value")).toBe("A description"); - }); - - it("renders endpoints logos", () => { - const wrapper = wrap({ item, endpoints, instancesDetails }); - expect(wrapper.find("sourceLogo").prop("endpoint")).toBe("openstack"); - expect(wrapper.find("targetLogo").prop("endpoint")).toBe("azure"); - }); - - it("renders loading", () => { - const wrapper = wrap({ item: {}, endpoints: [], loading: true }); - expect(wrapper.find("loading").length).toBe(1); - }); -}); diff --git a/src/components/modules/TransferModule/MigrationDetailsContent/MigrationDetailsContent.spec.tsx b/src/components/modules/TransferModule/MigrationDetailsContent/MigrationDetailsContent.spec.tsx new file mode 100644 index 00000000..c2e4bc9a --- /dev/null +++ b/src/components/modules/TransferModule/MigrationDetailsContent/MigrationDetailsContent.spec.tsx @@ -0,0 +1,74 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; + +import MigrationDetailsContent from "."; +import { MIGRATION_ITEM_DETAILS_MOCK } from "@tests/mocks/TransferMock"; +import { MINION_POOL_MOCK } from "@tests/mocks/MinionPoolMock"; +import { STORAGE_BACKEND_MOCK } from "@tests/mocks/StoragesMock"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; +import { NETWORK_MOCK } from "@tests/mocks/NetworksMock"; +import { + OPENSTACK_ENDPOINT_MOCK, + VMWARE_ENDPOINT_MOCK, +} from "@tests/mocks/EndpointsMock"; + +jest.mock("@src/components/modules/EndpointModule/EndpointLogos", () => ({ + __esModule: true, + default: (props: any) =>
{props.endpoint}
, +})); +jest.mock("react-router-dom", () => ({ Link: "a" })); + +describe("MigrationDetailsContent", () => { + let defaultProps: MigrationDetailsContent["props"]; + + beforeEach(() => { + defaultProps = { + item: MIGRATION_ITEM_DETAILS_MOCK, + itemId: MIGRATION_ITEM_DETAILS_MOCK.id, + minionPools: [MINION_POOL_MOCK], + detailsLoading: false, + storageBackends: [STORAGE_BACKEND_MOCK], + instancesDetails: [INSTANCE_MOCK], + instancesDetailsLoading: false, + networks: [NETWORK_MOCK], + sourceSchema: [], + sourceSchemaLoading: false, + destinationSchema: [], + destinationSchemaLoading: false, + endpoints: [OPENSTACK_ENDPOINT_MOCK, VMWARE_ENDPOINT_MOCK], + page: "", + onDeleteMigrationClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(MIGRATION_ITEM_DETAILS_MOCK.id)).toBeTruthy(); + }); + + it("renders tasks page", () => { + const { getByText } = render( + + ); + expect( + getByText( + MIGRATION_ITEM_DETAILS_MOCK.tasks[0].task_type.replace("_", " ") + ) + ).toBeTruthy(); + }); +}); diff --git a/src/components/modules/TransferModule/MigrationDetailsContent/MigrationDetailsContent.tsx b/src/components/modules/TransferModule/MigrationDetailsContent/MigrationDetailsContent.tsx index e931c1ca..27cb7711 100644 --- a/src/components/modules/TransferModule/MigrationDetailsContent/MigrationDetailsContent.tsx +++ b/src/components/modules/TransferModule/MigrationDetailsContent/MigrationDetailsContent.tsx @@ -12,23 +12,22 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled from "styled-components"; -import Button from "@src/components/ui/Button"; +import { MigrationItemDetails } from "@src/@types/MainItem"; +import { MinionPool } from "@src/@types/MinionPool"; +import { Network } from "@src/@types/Network"; import DetailsNavigation from "@src/components/modules/NavigationModule/DetailsNavigation"; import MainDetails from "@src/components/modules/TransferModule/MainDetails"; import Tasks from "@src/components/modules/TransferModule/Tasks"; +import { ThemeProps } from "@src/components/Theme"; +import Button from "@src/components/ui/Button"; import type { Instance } from "@src/@types/Instance"; import type { Endpoint, StorageBackend } from "@src/@types/Endpoint"; import type { Field } from "@src/@types/Field"; -import { MigrationItemDetails } from "@src/@types/MainItem"; -import { MinionPool } from "@src/@types/MinionPool"; -import { Network } from "@src/@types/Network"; -import { ThemeProps } from "@src/components/Theme"; - const Wrapper = styled.div` display: flex; justify-content: center; diff --git a/src/components/modules/TransferModule/MigrationDetailsContent/test.tsx b/src/components/modules/TransferModule/MigrationDetailsContent/test.tsx deleted file mode 100644 index 9b17ec00..00000000 --- a/src/components/modules/TransferModule/MigrationDetailsContent/test.tsx +++ /dev/null @@ -1,77 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import MigrationDetailsContent from "."; - -const wrap = props => - new TW(shallow(), "mdContent"); - -const tasks = [ - { - progress_updates: [ - { message: "the task has a progress of 50%", created_at: new Date() }, - { message: "the task is almost done", created_at: new Date() }, - ], - exception_details: "Exception details", - status: "RUNNING", - created_at: new Date(), - depends_on: ["depends on id"], - id: "task-2", - task_type: "Task name 2", - }, -]; -const endpoints = [ - { id: "endpoint-1", name: "Endpoint OPS", type: "openstack" }, - { id: "endpoint-2", name: "Endpoint AZURE", type: "azure" }, -]; -const item = { - origin_endpoint_id: "endpoint-1", - destination_endpoint_id: "endpoint-2", - id: "item-id", - created_at: new Date(2017, 10, 24, 16, 15), - tasks, - destination_environment: { description: "A description" }, - type: "Migration", -}; - -describe("MigrationDetailsContent Component", () => { - it("renders main details page", () => { - const wrapper = wrap({ endpoints, item, page: "" }); - expect(wrapper.find("mainDetails").prop("item").id).toBe("item-id"); - }); - - it("renders tasks page", () => { - const wrapper = wrap({ endpoints, item, page: "tasks" }); - expect(wrapper.find("tasks").prop("items")[0].id).toBe("task-2"); - }); - - it("renders details loading", () => { - const wrapper = wrap({ endpoints, item, page: "", detailsLoading: true }); - expect(wrapper.find("mainDetails").prop("loading")).toBe(true); - }); - - it("dispatches delete click", () => { - const onDeleteMigrationClick = sinon.spy(); - const wrapper = wrap({ endpoints, item, page: "", onDeleteMigrationClick }); - wrapper - .find("mainDetails") - .prop("bottomControls") - .props.children.props.onClick(); - expect(onDeleteMigrationClick.calledOnce).toBe(true); - }); -}); diff --git a/src/components/modules/TransferModule/ReplicaDetailsContent/ReplicaDetailsContent.spec.tsx b/src/components/modules/TransferModule/ReplicaDetailsContent/ReplicaDetailsContent.spec.tsx new file mode 100644 index 00000000..12728dd4 --- /dev/null +++ b/src/components/modules/TransferModule/ReplicaDetailsContent/ReplicaDetailsContent.spec.tsx @@ -0,0 +1,131 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import Schedule from "@src/components/modules/TransferModule/Schedule"; +import ScheduleStore from "@src/stores/ScheduleStore"; +import { render } from "@testing-library/react"; +import { + OPENSTACK_ENDPOINT_MOCK, + VMWARE_ENDPOINT_MOCK, +} from "@tests/mocks/EndpointsMock"; +import { + EXECUTION_MOCK, + EXECUTION_TASKS_MOCK, +} from "@tests/mocks/ExecutionsMock"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; +import { MINION_POOL_MOCK } from "@tests/mocks/MinionPoolMock"; +import { NETWORK_MOCK } from "@tests/mocks/NetworksMock"; +import { STORAGE_BACKEND_MOCK } from "@tests/mocks/StoragesMock"; +import { REPLICA_ITEM_DETAILS_MOCK } from "@tests/mocks/TransferMock"; + +import ReplicaDetailsContent from "./"; + +const scheduleStoreMock = jest.createMockFromModule( + "@src/stores/ScheduleStore" +); + +jest.mock("@src/components/modules/EndpointModule/EndpointLogos", () => ({ + __esModule: true, + default: (props: any) =>
{props.endpoint}
, +})); +jest.mock("react-router-dom", () => ({ Link: "a" })); +jest.mock("@src/utils/Config", () => ({ + config: { + providerSortPriority: {}, + providerNames: { + openstack: "OpenStack", + vmware_vsphere: "VMware vSphere", + }, + providersDisabledExecuteOptions: ["metal"], + }, +})); +jest.mock("@src/components/modules/TransferModule/Schedule", () => ({ + __esModule: true, + default: (props: Schedule["props"]) => ( +
props.onTimezoneChange("utc")} + > + Timezone: {props.timezone} +
+ ), +})); + +describe("ReplicaDetailsContent", () => { + let defaultProps: ReplicaDetailsContent["props"]; + + beforeEach(() => { + defaultProps = { + item: REPLICA_ITEM_DETAILS_MOCK, + itemId: REPLICA_ITEM_DETAILS_MOCK.id, + endpoints: [OPENSTACK_ENDPOINT_MOCK, VMWARE_ENDPOINT_MOCK], + sourceSchema: [], + sourceSchemaLoading: false, + destinationSchema: [], + destinationSchemaLoading: false, + networks: [NETWORK_MOCK], + instancesDetails: [INSTANCE_MOCK], + instancesDetailsLoading: false, + scheduleStore: scheduleStoreMock, + page: "", + detailsLoading: false, + executions: [EXECUTION_MOCK], + executionsLoading: false, + executionsTasks: [EXECUTION_TASKS_MOCK], + executionsTasksLoading: false, + minionPools: [MINION_POOL_MOCK], + storageBackends: [STORAGE_BACKEND_MOCK], + onExecutionChange: jest.fn(), + onCancelExecutionClick: jest.fn(), + onDeleteExecutionClick: jest.fn(), + onExecuteClick: jest.fn(), + onCreateMigrationClick: jest.fn(), + onDeleteReplicaClick: jest.fn(), + onAddScheduleClick: jest.fn(), + onScheduleChange: jest.fn(), + onScheduleRemove: jest.fn(), + onScheduleSave: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(REPLICA_ITEM_DETAILS_MOCK.id)).toBeTruthy(); + }); + + it("renders executions page", () => { + const { getByText } = render( + + ); + expect(getByText(EXECUTION_MOCK.id)).toBeTruthy(); + }); + + it("rendes schedules page", () => { + const { getByTestId } = render( + + ); + expect(getByTestId("ScheduleComponent")).toBeTruthy(); + }); + + it("fires timezone change", () => { + const { getByTestId, getByText } = render( + + ); + expect(getByText("Timezone: local")).toBeTruthy(); + getByTestId("ScheduleComponent").click(); + expect(getByText("Timezone: utc")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/TransferModule/ReplicaDetailsContent/ReplicaDetailsContent.tsx b/src/components/modules/TransferModule/ReplicaDetailsContent/ReplicaDetailsContent.tsx index 72355637..013e295f 100644 --- a/src/components/modules/TransferModule/ReplicaDetailsContent/ReplicaDetailsContent.tsx +++ b/src/components/modules/TransferModule/ReplicaDetailsContent/ReplicaDetailsContent.tsx @@ -12,27 +12,27 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +import { observer } from "mobx-react"; import React from "react"; import styled from "styled-components"; -import { observer } from "mobx-react"; -import scheduleStore from "@src/stores/ScheduleStore"; -import Button from "@src/components/ui/Button"; +import { ReplicaItemDetails } from "@src/@types/MainItem"; +import { MinionPool } from "@src/@types/MinionPool"; import DetailsNavigation from "@src/components/modules/NavigationModule/DetailsNavigation"; -import MainDetails from "@src/components/modules/TransferModule/MainDetails"; import Executions from "@src/components/modules/TransferModule/Executions"; +import MainDetails from "@src/components/modules/TransferModule/MainDetails"; import Schedule from "@src/components/modules/TransferModule/Schedule"; +import { ThemeProps } from "@src/components/Theme"; +import Button from "@src/components/ui/Button"; +import scheduleStore from "@src/stores/ScheduleStore"; +import configLoader from "@src/utils/Config"; + import type { Instance } from "@src/@types/Instance"; import type { Endpoint, StorageBackend } from "@src/@types/Endpoint"; import type { Execution, ExecutionTasks } from "@src/@types/Execution"; import type { Network } from "@src/@types/Network"; import type { Field } from "@src/@types/Field"; import type { Schedule as ScheduleType } from "@src/@types/Schedule"; -import { ReplicaItemDetails } from "@src/@types/MainItem"; -import { MinionPool } from "@src/@types/MinionPool"; -import { ThemeProps } from "@src/components/Theme"; -import configLoader from "@src/utils/Config"; - const Wrapper = styled.div` display: flex; justify-content: center; diff --git a/src/components/modules/TransferModule/ReplicaDetailsContent/test.tsx b/src/components/modules/TransferModule/ReplicaDetailsContent/test.tsx deleted file mode 100644 index 4ae28c55..00000000 --- a/src/components/modules/TransferModule/ReplicaDetailsContent/test.tsx +++ /dev/null @@ -1,134 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import ReplicaDetailsContent from "."; - -const wrap = props => - new TW(shallow(), "rdContent"); - -const endpoints = [ - { id: "endpoint-1", name: "Endpoint OPS", type: "openstack" }, - { id: "endpoint-2", name: "Endpoint AZURE", type: "azure" }, -]; -const item = { - origin_endpoint_id: "endpoint-1", - destination_endpoint_id: "endpoint-2", - id: "item-id", - created_at: new Date(2017, 10, 24, 16, 15), - destination_environment: { description: "A description" }, - type: "Replica", - executions: [ - { id: "execution-1", status: "ERROR", created_at: new Date() }, - { id: "execution-2", status: "COMPLETED", created_at: new Date() }, - { id: "execution-2-1", status: "CANCELED", created_at: new Date() }, - { id: "execution-3", status: "RUNNING", created_at: new Date() }, - ], -}; - -describe("ReplicaDetailsContent Component", () => { - it("renders main details page", () => { - const wrapper = wrap({ endpoints, item, page: "" }); - expect(wrapper.find("mainDetails").prop("item").id).toBe("item-id"); - }); - - it("renders executions page", () => { - const wrapper = wrap({ endpoints, item, page: "executions" }); - expect(wrapper.find("executions").prop("item").executions[1].id).toBe( - "execution-2" - ); - }); - - it("renders details loading", () => { - const wrapper = wrap({ endpoints, item, page: "", detailsLoading: true }); - expect(wrapper.find("mainDetails").prop("loading")).toBe(true); - }); - - it("renders schedule page", () => { - const wrapper = wrap({ - endpoints, - item, - page: "schedule", - scheduleStore: { schedules: [] }, - }); - expect(wrapper.find("schedule").prop("schedules").length).toBe(0); - }); - - it("has `Create migration` button disabled if endpoint is missing", () => { - const wrapper = wrap({ endpoints, item: null, page: "" }); - const bottomControls = new TW( - shallow(wrapper.find("mainDetails").prop("bottomControls")), - "rdContent" - ); - expect(bottomControls.find("createButton").prop("disabled")).toBe(true); - }); - - it("has `Create migration` button enabled if the last status is completed", () => { - const newItem = { - ...item, - executions: [ - ...item.executions, - { id: "execution-4", status: "COMPLETED", created_at: new Date() }, - ], - }; - const wrapper = wrap({ endpoints, item: newItem, page: "" }); - const bottomControls = new TW( - shallow(wrapper.find("mainDetails").prop("bottomControls")), - "rdContent" - ); - expect(bottomControls.find("createButton").prop("disabled")).toBe(false); - }); - - it("dispaches create migration click", () => { - const onCreateMigrationClick = sinon.spy(); - const wrapper = wrap({ endpoints, item, page: "", onCreateMigrationClick }); - const bottomControls = new TW( - shallow(wrapper.find("mainDetails").prop("bottomControls")), - "rdContent" - ); - bottomControls.find("createButton").click(); - expect(onCreateMigrationClick.calledOnce).toBe(true); - }); - - it("has `Create migration` button disabled if endpoint is missing and last status is completed", () => { - const newItem = { - ...item, - origin_endpoint_id: "missing", - executions: [ - ...item.executions, - { id: "execution-4", status: "COMPLETED", created_at: new Date() }, - ], - }; - const wrapper = wrap({ endpoints, item: newItem, page: "" }); - const bottomControls = new TW( - shallow(wrapper.find("mainDetails").prop("bottomControls")), - "rdContent" - ); - expect(bottomControls.find("createButton").prop("disabled")).toBe(true); - }); - - it("dispatches delete click", () => { - const onDeleteReplicaClick = sinon.spy(); - const wrapper = wrap({ endpoints, item, page: "", onDeleteReplicaClick }); - const bottomControls = new TW( - shallow(wrapper.find("mainDetails").prop("bottomControls")), - "rdContent" - ); - bottomControls.find("deleteButton").click(); - expect(onDeleteReplicaClick.calledOnce).toBe(true); - }); -}); diff --git a/src/components/modules/TransferModule/ReplicaExecutionOptions/ReplicaExecutionOptions.spec.tsx b/src/components/modules/TransferModule/ReplicaExecutionOptions/ReplicaExecutionOptions.spec.tsx new file mode 100644 index 00000000..40e11391 --- /dev/null +++ b/src/components/modules/TransferModule/ReplicaExecutionOptions/ReplicaExecutionOptions.spec.tsx @@ -0,0 +1,75 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { fireEvent, render } from "@testing-library/react"; +import TestUtils from "@tests/TestUtils"; + +import ReplicaExecutionOptions from "./"; + +jest.mock("@src/plugins/default/ContentPlugin", () => jest.fn(() => null)); + +describe("ReplicaExecutionOptions", () => { + let defaultProps: ReplicaExecutionOptions["props"]; + + beforeEach(() => { + defaultProps = { + options: { + shutdown_instances: true, + }, + disableExecutionOptions: false, + onChange: jest.fn(), + executionLabel: "Execute", + onCancelClick: jest.fn(), + onExecuteClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(defaultProps.executionLabel)).toBeTruthy(); + }); + + it("executes on Enter", () => { + render(); + fireEvent.keyDown(document.body, { key: "Enter" }); + expect(defaultProps.onExecuteClick).toHaveBeenCalled(); + }); + + it("returns original field value if options is null", () => { + render( + + ); + expect(TestUtils.select("Switch__Wrapper")?.textContent).toBe("No"); + }); + + it("handles value change", () => { + render(); + fireEvent.click(TestUtils.select("Switch__InputWrapper")!); + expect(defaultProps.onChange).toHaveBeenCalledWith( + "shutdown_instances", + false + ); + }); + + it("handles execute click", () => { + const { getByText } = render(); + fireEvent.click(getByText(defaultProps.executionLabel)); + expect(defaultProps.onExecuteClick).toHaveBeenCalled(); + }); +}); diff --git a/src/components/modules/TransferModule/ReplicaExecutionOptions/test.tsx b/src/components/modules/TransferModule/ReplicaExecutionOptions/test.tsx deleted file mode 100644 index f30e9490..00000000 --- a/src/components/modules/TransferModule/ReplicaExecutionOptions/test.tsx +++ /dev/null @@ -1,70 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import ReplicaExecutionOptions from "."; - -import { executionOptions } from "@src/constants"; - -const wrap = props => - new TW(shallow(), "reOptions"); - -describe("ReplicaExecutionOptions Component", () => { - it("renders executionOptions from config", () => { - const wrapper = wrap(); - executionOptions.forEach(option => { - expect(wrapper.find(`option-${option.name}`).prop("name")).toBe( - option.name - ); - }); - }); - - it("renders executionOptions with default values", () => { - const wrapper = wrap(); - executionOptions.forEach(option => { - expect(wrapper.find(`option-${option.name}`).prop("value")).toBe( - option.defaultValue || undefined - ); - }); - }); - - it("renders executionOptions with given values", () => { - const wrapper = wrap({ options: { shutdown_instances: true } }); - expect(wrapper.find("option-shutdown_instances").prop("value")).toBe(true); - }); - - it("dispaches cancel click", () => { - const onCancelClick = sinon.spy(); - const wrapper = wrap({ onCancelClick }); - wrapper.find("cancelButton").click(); - expect(onCancelClick.calledOnce).toBe(true); - }); - - it("renders custom execution button label", () => { - const wrapper = wrap({ executionLabel: "custom_exec" }); - expect(wrapper.find("execButton").shallow.dive().dive().text()).toBe( - "custom_exec" - ); - }); - - it("dispaches execution click", () => { - const onExecuteClick = sinon.spy(); - const wrapper = wrap({ onExecuteClick }); - wrapper.find("execButton").click(); - expect(onExecuteClick.args[0][0][0].name).toBe(executionOptions[0].name); - }); -}); diff --git a/src/components/modules/TransferModule/ReplicaMigrationOptions/ReplicaMigrationOptions.spec.tsx b/src/components/modules/TransferModule/ReplicaMigrationOptions/ReplicaMigrationOptions.spec.tsx new file mode 100644 index 00000000..f78f5728 --- /dev/null +++ b/src/components/modules/TransferModule/ReplicaMigrationOptions/ReplicaMigrationOptions.spec.tsx @@ -0,0 +1,154 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import WizardScripts from "@src/components/modules/WizardModule/WizardScripts"; +import { fireEvent, render } from "@testing-library/react"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; +import { MINION_POOL_MOCK } from "@tests/mocks/MinionPoolMock"; +import { REPLICA_ITEM_DETAILS_MOCK } from "@tests/mocks/TransferMock"; +import TestUtils from "@tests/TestUtils"; + +import ReplicaMigrationOptions from "./"; + +jest.mock("@src/plugins/default/ContentPlugin", () => jest.fn(() => null)); +jest.mock("@src/components/modules/WizardModule/WizardScripts", () => ({ + __esModule: true, + default: (props: WizardScripts["props"]) => ( +
+
+ {props.uploadedScripts.map(s => s.scriptContent).join(", ")} +
+
{ + props.onScriptDataRemove(props.uploadedScripts[0]); + }} + /> +
+ {props.removedScripts.map(s => s.scriptContent).join(", ")} +
+
{ + props.onCancelScript("windows", null); + props.scrollableRef && + props.scrollableRef(null as any as HTMLElement); + }} + /> +
{ + props.onScriptUpload({ + scriptContent: `script-content-${Math.random()}`, + fileName: `script-name.ps1`, + global: "windows", + }); + }} + /> +
+ ), +})); + +describe("ReplicaMigrationOptions", () => { + let defaultProps: ReplicaMigrationOptions["props"]; + + beforeEach(() => { + defaultProps = { + instances: [INSTANCE_MOCK], + transferItem: REPLICA_ITEM_DETAILS_MOCK, + minionPools: [ + MINION_POOL_MOCK, + { ...MINION_POOL_MOCK, id: "pool2", name: "Pool2" }, + ], + loadingInstances: false, + onCancelClick: jest.fn(), + onMigrateClick: jest.fn(), + onResizeUpdate: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Migrate")).toBeTruthy(); + }); + + it("executes on Enter", () => { + render(); + fireEvent.keyDown(document.body, { key: "Enter" }); + expect(defaultProps.onMigrateClick).toHaveBeenCalled(); + }); + + it("calls onResizeUpdate on selectedBarButton state change", () => { + render(); + fireEvent.click(TestUtils.selectAll("ToggleButtonBar__Item-")[1]); + expect(defaultProps.onResizeUpdate).toHaveBeenCalled(); + }); + + it("handles value change", () => { + render(); + expect(TestUtils.select("Switch__Wrapper")?.textContent).toBe("Yes"); + fireEvent.click(TestUtils.select("Switch__InputWrapper")!); + expect(TestUtils.select("Switch__Wrapper")?.textContent).toBe("No"); + }); + + it("handles script operations", () => { + const { getByTestId } = render( + + ); + fireEvent.click(TestUtils.selectAll("ToggleButtonBar__Item-")[1]); + fireEvent.click(getByTestId("ScriptsUpload")); + expect(getByTestId("ScriptsUploaded").textContent).toContain( + "script-content" + ); + fireEvent.click(getByTestId("ScriptsCancel")); + expect(getByTestId("ScriptsUploaded").textContent).toBe(""); + + fireEvent.click(getByTestId("ScriptsUpload")); + expect(getByTestId("ScriptsUploaded").textContent).toContain( + "script-content" + ); + expect(getByTestId("ScriptsRemoved").textContent).toBe(""); + fireEvent.click(getByTestId("ScriptsRemove")); + expect(getByTestId("ScriptsRemoved").textContent).toContain( + "script-content" + ); + }); + + it("doesn't render minion pool mappings", () => { + const { rerender } = render(); + expect(document.body.textContent).toContain("Minion Pool Mappings"); + + rerender(); + expect(document.body.textContent).not.toContain("Minion Pool Mappings"); + }); + + it("changes minion pool mappings value", () => { + render(); + fireEvent.click(TestUtils.select("DropdownButton__Wrapper-")!); + const dropdownItem = TestUtils.selectAll("Dropdown__ListItem-")[2]; + expect(dropdownItem.textContent).toBe("Pool2"); + fireEvent.click(dropdownItem); + expect(TestUtils.select("DropdownButton__Label-")?.textContent).toBe( + "Pool2" + ); + }); + + it("handles migrate click", () => { + const { getByText } = render(); + fireEvent.click(getByText("Migrate")); + expect(defaultProps.onMigrateClick).toHaveBeenCalled(); + }); +}); diff --git a/src/components/modules/TransferModule/ReplicaMigrationOptions/ReplicaMigrationOptions.tsx b/src/components/modules/TransferModule/ReplicaMigrationOptions/ReplicaMigrationOptions.tsx index 69577c24..0dce1170 100644 --- a/src/components/modules/TransferModule/ReplicaMigrationOptions/ReplicaMigrationOptions.tsx +++ b/src/components/modules/TransferModule/ReplicaMigrationOptions/ReplicaMigrationOptions.tsx @@ -12,28 +12,27 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled from "styled-components"; +import { TransferItemDetails } from "@src/@types/MainItem"; +import { MinionPool } from "@src/@types/MinionPool"; +import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from "@src/components/modules/WizardModule/WizardOptions"; +import WizardScripts from "@src/components/modules/WizardModule/WizardScripts"; +import { ThemeProps } from "@src/components/Theme"; import Button from "@src/components/ui/Button"; import FieldInput from "@src/components/ui/FieldInput"; +import LoadingButton from "@src/components/ui/LoadingButton"; import ToggleButtonBar from "@src/components/ui/ToggleButtonBar"; -import WizardScripts from "@src/components/modules/WizardModule/WizardScripts"; - -import LabelDictionary from "@src/utils/LabelDictionary"; import KeyboardManager from "@src/utils/KeyboardManager"; +import LabelDictionary from "@src/utils/LabelDictionary"; -import type { Field } from "@src/@types/Field"; -import type { Instance, InstanceScript } from "@src/@types/Instance"; -import { TransferItemDetails } from "@src/@types/MainItem"; -import { MinionPool } from "@src/@types/MinionPool"; -import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from "@src/components/modules/WizardModule/WizardOptions"; -import { ThemeProps } from "@src/components/Theme"; -import replicaMigrationFields from "./replicaMigrationFields"; import replicaMigrationImage from "./images/replica-migration.svg"; -import LoadingButton from "@src/components/ui/LoadingButton"; +import replicaMigrationFields from "./replicaMigrationFields"; +import type { Field } from "@src/@types/Field"; +import type { Instance, InstanceScript } from "@src/@types/Instance"; const Wrapper = styled.div` display: flex; flex-direction: column; @@ -172,7 +171,7 @@ class ReplicaMigrationOptions extends React.Component { }); } - handleCanceScript(global: string | null, instanceName: string | null) { + handleCancelScript(global: string | null, instanceName: string | null) { this.setState(prevState => ({ uploadedScripts: prevState.uploadedScripts.filter(s => global ? s.global !== global : s.instanceId !== instanceName @@ -278,7 +277,7 @@ class ReplicaMigrationOptions extends React.Component { this.handleScriptRemove(s); }} onCancelScript={(g, i) => { - this.handleCanceScript(g, i); + this.handleCancelScript(g, i); }} uploadedScripts={this.state.uploadedScripts} removedScripts={this.state.removedScripts} diff --git a/src/components/modules/TransferModule/ReplicaMigrationOptions/test.tsx b/src/components/modules/TransferModule/ReplicaMigrationOptions/test.tsx deleted file mode 100644 index c02d12db..00000000 --- a/src/components/modules/TransferModule/ReplicaMigrationOptions/test.tsx +++ /dev/null @@ -1,50 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import ReplicaMigrationOptions from "."; - -const wrap = props => - new TW( - shallow( - {}} - loadingInstances={false} - defaultSkipOsMorphing={false} - {...props} - /> - ), - "rmOptions" - ); - -describe("ReplicaMigrationOptions Component", () => { - it("dispatches cancel click", () => { - const onCancelClick = sinon.spy(); - const wrapper = wrap({ onCancelClick }); - wrapper.find("cancelButton").click(); - expect(onCancelClick.calledOnce).toBe(true); - }); - - it("dispatches migrate click", () => { - const onMigrateClick = sinon.spy(); - const wrapper = wrap({ onMigrateClick }); - wrapper.find("execButton").click(); - expect(onMigrateClick.args[0][0][0].name).toBe("clone_disks"); - expect(onMigrateClick.args[0][0][0].value).toBe(true); - }); -}); diff --git a/src/components/modules/TransferModule/Schedule/Schedule.spec.tsx b/src/components/modules/TransferModule/Schedule/Schedule.spec.tsx new file mode 100644 index 00000000..c8f09029 --- /dev/null +++ b/src/components/modules/TransferModule/Schedule/Schedule.spec.tsx @@ -0,0 +1,245 @@ +/* +Copyright (C) 2023 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 . +*/ + +import { DateTime } from "luxon"; +import React from "react"; + +import DateUtils from "@src/utils/DateUtils"; +import { render } from "@testing-library/react"; +import { SCHEDULE_MOCK } from "@tests/mocks/SchedulesMock"; +import TestUtils from "@tests/TestUtils"; + +import Schedule from "./"; + +jest.mock("@src/plugins/default/ContentPlugin", () => jest.fn(() => null)); + +describe("Schedule", () => { + let defaultProps: Schedule["props"]; + + beforeEach(() => { + defaultProps = { + schedules: [SCHEDULE_MOCK], + unsavedSchedules: [], + timezone: "utc", + disableExecutionOptions: false, + onTimezoneChange: jest.fn(), + onAddScheduleClick: jest.fn(), + onChange: jest.fn(), + onRemove: jest.fn(), + onSaveSchedule: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Add Schedule")).toBeTruthy(); + expect(TestUtils.select("Schedule__Timezone-")?.textContent).toContain( + "all times inUTC" + ); + }); + + it("deletes a schedule", () => { + render( + + ); + TestUtils.select("ScheduleItem__DeleteButton-")?.click(); + expect(TestUtils.select("AlertModal__Message-")?.textContent).toContain( + "delete this schedule?" + ); + const modal = TestUtils.select("AlertModal__Wrapper-")!; + Array.from(modal.querySelectorAll("button")) + .find(b => b.textContent === "Yes") + ?.click(); + expect(defaultProps.onRemove).toHaveBeenCalledWith(SCHEDULE_MOCK.id); + }); + + it("dismisses the delete modal", () => { + render( + + ); + TestUtils.select("ScheduleItem__DeleteButton-")?.click(); + expect(TestUtils.select("AlertModal__Message-")?.textContent).toContain( + "delete this schedule?" + ); + const modal = TestUtils.select("AlertModal__Wrapper-")!; + Array.from(modal.querySelectorAll("button")) + .find(b => b.textContent === "No") + ?.click(); + expect(defaultProps.onRemove).not.toHaveBeenCalled(); + }); + + it("changes execution options", () => { + render( + + ); + const optionsButton = Array.from(document.querySelectorAll("button")).find( + el => el.textContent === "•••" + ); + optionsButton?.click(); + expect(TestUtils.select("Modal__Title-")?.textContent).toBe( + "Execution options" + ); + const modal = TestUtils.select("ReplicaExecutionOptions__Wrapper-")!; + TestUtils.select("Switch__InputWrapper-", modal)?.click(); + const yesButton = Array.from(modal.querySelectorAll("button")).find( + el => el.textContent === "Save" + ); + expect(yesButton).toBeTruthy(); + yesButton!.click(); + expect(defaultProps.onChange).toHaveBeenCalled(); + }); + + it("dismisses the execution options modal", () => { + render( + + ); + const optionsButton = Array.from(document.querySelectorAll("button")).find( + el => el.textContent === "•••" + ); + optionsButton?.click(); + expect(TestUtils.select("Modal__Title-")?.textContent).toBe( + "Execution options" + ); + const modal = TestUtils.select("ReplicaExecutionOptions__Wrapper-")!; + const noButton = Array.from(modal.querySelectorAll("button")).find( + el => el.textContent === "Cancel" + ); + expect(noButton).toBeTruthy(); + noButton!.click(); + expect(defaultProps.onChange).not.toHaveBeenCalled(); + }); + + it("adds a schedule with UTC", () => { + const { getByText } = render(); + getByText("Add Schedule").click(); + expect(defaultProps.onAddScheduleClick).toHaveBeenCalledWith({ + schedule: { hour: 0, minute: 0 }, + }); + }); + + it("adds a schedule with local timezone", () => { + const { getByText } = render( + + ); + getByText("Add Schedule").click(); + expect(defaultProps.onAddScheduleClick).toHaveBeenCalledWith({ + schedule: { hour: DateUtils.getUtcHour(0), minute: 0 }, + }); + }); + + it("renders loading", () => { + render(); + expect(TestUtils.select("Schedule__LoadingWrapper")).toBeTruthy(); + }); + + it("changes schedule", () => { + render( + + ); + const monthDropdown = TestUtils.select("DropdownButton__Wrapper-")!; + expect(monthDropdown).toBeTruthy(); + monthDropdown.click(); + const february = Array.from( + TestUtils.selectAll("Dropdown__ListItem-")! + ).find(el => el.textContent === DateTime.local(2023, 2).monthLong); + expect(february).toBeTruthy(); + february!.click(); + expect(defaultProps.onChange).toHaveBeenCalledWith( + SCHEDULE_MOCK.id, + { + schedule: { month: 2 }, + }, + undefined + ); + }); + + it("saves schedule", () => { + render( + + ); + TestUtils.select("ScheduleItem__SaveButton-")?.click(); + expect(defaultProps.onSaveSchedule).toHaveBeenCalled(); + }); + + it("handles saving", () => { + render(); + expect(TestUtils.select("ScheduleItem__SavingIcon-")).toBeTruthy(); + }); + + it("handles enabling", () => { + render(); + expect(TestUtils.select("ScheduleItem__EnablingIcon-")).toBeTruthy(); + }); + + it("handles deleting", () => { + render(); + expect(TestUtils.select("ScheduleItem__DeletingIcon-")).toBeTruthy(); + }); + + it("renders primary no schedules", () => { + render(); + expect(TestUtils.select("Schedule__NoSchedules-")?.textContent).toContain( + "has no Schedules" + ); + }); + + it("renders secondary no schedules", () => { + render(); + expect(TestUtils.select("Schedule__NoSchedules-")?.textContent).toContain( + "Schedule this Replica" + ); + }); + + it("adds schedule from no schedules", () => { + const { getByText, rerender } = render( + + ); + getByText("Add Schedule").click(); + expect(defaultProps.onAddScheduleClick).toHaveBeenCalled(); + + rerender(); + expect(getByText("Adding ...")).toBeTruthy(); + }); + + it("changes timezone", () => { + render(); + const timezoneDropdown = TestUtils.select( + "DropdownLink__LinkButton-", + TestUtils.select("Schedule__Timezone-")! + )!; + expect(timezoneDropdown).toBeTruthy(); + timezoneDropdown.click(); + TestUtils.select("DropdownLink__ListItem-")!.click(); + expect(defaultProps.onTimezoneChange).toHaveBeenCalled(); + }); +}); diff --git a/src/components/modules/TransferModule/Schedule/Schedule.tsx b/src/components/modules/TransferModule/Schedule/Schedule.tsx index b2e4038a..c95edd99 100644 --- a/src/components/modules/TransferModule/Schedule/Schedule.tsx +++ b/src/components/modules/TransferModule/Schedule/Schedule.tsx @@ -133,7 +133,7 @@ type State = { executionOptions: { [prop: string]: any } | null; }; -const colWidths = ["6%", "18%", "10%", "18%", "10%", "10%", "23%", "5%"]; +const COL_WIDTHS = ["6%", "18%", "10%", "18%", "10%", "10%", "23%", "5%"]; @observer class Schedule extends React.Component { static defaultProps = { @@ -237,7 +237,7 @@ class Schedule extends React.Component { return (
{headerLabels.map((l, i) => ( - + {l} ))} @@ -255,7 +255,7 @@ class Schedule extends React.Component { {this.props.schedules.map(schedule => ( . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import moment from "moment"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import Schedule from "."; - -const wrap = props => new TW(shallow(), "schedule"); - -const schedules = [ - { - id: "s-1", - schedule: { dom: 4, dow: 3, month: 2, hour: 13, minute: 29 }, - expiration_date: new Date(2017, 10, 27, 17, 19), - }, - { - id: "s-2", - enabled: true, - schedule: { dom: 2, dow: 3, month: 2, hour: 13, minute: 29 }, - expiration_date: new Date(), - }, -]; - -describe("Schedule Component", () => { - it("renders no schedules", () => { - const wrapper = wrap({ schedules: [] }); - expect(wrapper.findText("noScheduleTitle")).toBe( - "This Replica has no Schedules." - ); - }); - - it("dispaches no schedules `Add schedule` click", () => { - const onAddScheduleClick = sinon.spy(); - const wrapper = wrap({ onAddScheduleClick }); - wrapper.find("noScheduleAddButton").click(); - expect(onAddScheduleClick.calledOnce).toBe(true); - }); - - it("renders correct number of schedules", () => { - const wrapper = wrap({ schedules }); - schedules.forEach(schedule => { - expect(wrapper.find(`item-${schedule.id}`).prop("item").id).toBe( - schedule.id - ); - }); - }); - - it("dispatches timezone change", () => { - const onTimezoneChange = sinon.spy(); - const wrapper = wrap({ schedules, onTimezoneChange }); - wrapper - .find("timezoneDropdown") - .simulate("change", { value: schedules[0] }); - expect(onTimezoneChange.calledOnce).toBe(true); - }); - - it("dispatches Add schedule click from list of schedules with local timezone", () => { - const onAddScheduleClick = sinon.spy(); - const wrapper = wrap({ schedules, onAddScheduleClick, timezone: "local" }); - wrapper.find("addScheduleButton").click(); - const localHours = moment( - new Date( - new Date().getFullYear(), - new Date().getMonth(), - new Date().getDate() - ) - ) - .add(new Date().getTimezoneOffset(), "minutes") - .hours(); - expect(onAddScheduleClick.args[0][0].schedule.hour).toBe(localHours); - }); - - it("renders correct timezone in footer", () => { - const wrapper = wrap({ schedules, timezone: "utc" }); - expect(wrapper.find("timezoneDropdown").prop("selectedItem")).toBe("utc"); - }); - - it("has add button disabled while adding a schedule", () => { - const wrapper = wrap({ schedules, adding: true }); - expect(wrapper.find("addScheduleButton").prop("disabled")).toBe(true); - expect(wrapper.find("loadingStatus").length).toBe(0); - }); - - it("renders loading", () => { - const wrapper = wrap({ schedules: [], loading: true }); - expect(wrapper.find("loadingStatus").length).toBe(1); - }); -}); diff --git a/src/components/modules/TransferModule/ScheduleItem/ScheduleItem.spec.tsx b/src/components/modules/TransferModule/ScheduleItem/ScheduleItem.spec.tsx new file mode 100644 index 00000000..44429680 --- /dev/null +++ b/src/components/modules/TransferModule/ScheduleItem/ScheduleItem.spec.tsx @@ -0,0 +1,99 @@ +/* +Copyright (C) 2023 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 . +*/ + +import { DateTime } from "luxon"; +import React from "react"; + +import DateUtils from "@src/utils/DateUtils"; +import { render } from "@testing-library/react"; +import { SCHEDULE_MOCK } from "@tests/mocks/SchedulesMock"; +import TestUtils from "@tests/TestUtils"; + +import ScheduleItem from "./"; + +const COL_WIDTHS = ["6%", "18%", "10%", "18%", "10%", "10%", "23%", "5%"]; + +describe("ScheduleItem", () => { + let defaultProps: ScheduleItem["props"]; + + beforeEach(() => { + defaultProps = { + colWidths: COL_WIDTHS, + item: SCHEDULE_MOCK, + unsavedSchedules: [], + timezone: "local", + saving: false, + enabling: false, + deleting: false, + onChange: jest.fn(), + onSaveSchedule: jest.fn(), + onShowOptionsClick: jest.fn(), + onDeleteClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(DateTime.local(2023, 4, 4).weekdayLong!)).toBeTruthy(); + }); + + it("handles expiration date change", () => { + render( + + ); + TestUtils.selectAll("DropdownButton__Wrapper-")[5]?.click(); + const day = document.querySelector(".rdtDay.rdtNew") as HTMLElement; + expect(day).toBeTruthy(); + day.click(); + TestUtils.selectAll("DropdownButton__Wrapper-")[5]?.click(); + expect(defaultProps.onChange).toHaveBeenCalled(); + }); + + it.each` + fieldName | fieldIndex | value | valueIndex + ${"dom"} | ${1} | ${13} | ${13} + ${"dow"} | ${2} | ${4} | ${5} + ${"hour"} | ${3} | ${DateUtils.getUtcHour(2)} | ${3} + ${"minute"} | ${4} | ${30} | ${31} + `( + "handles $fieldName change", + ({ fieldName, fieldIndex, value, valueIndex }) => { + render( + + ); + TestUtils.selectAll("DropdownButton__Wrapper-")[fieldIndex]?.click(); + TestUtils.selectAll("Dropdown__ListItem-")[valueIndex]?.click(); + expect(defaultProps.onChange).toHaveBeenCalledWith({ + schedule: { [fieldName]: value }, + }); + } + ); + + it("enables item", () => { + render(); + TestUtils.select("Switch__InputWrapper-")!.click(); + expect(defaultProps.onChange).toHaveBeenCalledWith( + { + enabled: false, + }, + true + ); + }); +}); diff --git a/src/components/modules/TransferModule/ScheduleItem/test.tsx b/src/components/modules/TransferModule/ScheduleItem/test.tsx deleted file mode 100644 index 406c9eec..00000000 --- a/src/components/modules/TransferModule/ScheduleItem/test.tsx +++ /dev/null @@ -1,67 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import ScheduleItem from "."; - -const wrap = props => - new TW( - shallow( - - ), - "scheduleItem" - ); - -describe("ScheduleItem Component", () => { - it("should render all schedule properties", () => { - const wrapper = wrap({ - item: { - id: "schedule-1", - enabled: false, - schedule: { hour: 1, minute: 1, dow: 2, dom: 3, month: 5 }, - expiration_date: new Date(2018, 3, 25, 4, 0, 0), - shutdown_instances: false, - }, - }); - expect(wrapper.find("enabled").prop("checked")).toBe(false); - expect(wrapper.find("hourDropdown").prop("selectedItem").value).toBe(1); - expect(wrapper.find("minuteDropdown").prop("selectedItem").value).toBe(1); - expect(wrapper.find("dayOfWeekDropdown").prop("selectedItem").value).toBe( - 2 - ); - expect(wrapper.find("dayOfMonthDropdown").prop("selectedItem").value).toBe( - 3 - ); - expect(wrapper.find("monthDropdown").prop("selectedItem").value).toBe(5); - }); - - it("should highlight options button if options are changed", () => { - const wrapper = wrap({ - item: { - id: "schedule-1", - enabled: false, - schedule: { hour: 1, minute: 1, dow: 2, dom: 3, month: 5 }, - expiration_date: new Date(2018, 3, 25, 4, 0, 0), - shutdown_instances: true, - }, - }); - expect(wrapper.find("optionsButton").prop("hollow")).toBe(false); - }); -}); diff --git a/src/components/modules/TransferModule/TaskItem/TaskItem.spec.tsx b/src/components/modules/TransferModule/TaskItem/TaskItem.spec.tsx new file mode 100644 index 00000000..86cc108b --- /dev/null +++ b/src/components/modules/TransferModule/TaskItem/TaskItem.spec.tsx @@ -0,0 +1,134 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import DomUtils from "@src/utils/DomUtils"; +import { fireEvent, render } from "@testing-library/react"; +import { PROGRESS_UPDATE_MOCK, TASK_MOCK } from "@tests/mocks/ExecutionsMock"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; +import TestUtils from "@tests/TestUtils"; + +import TaskItem from "./"; + +const COLUMN_WIDTHS = ["26%", "18%", "36%", "20%"]; + +describe("TaskItem", () => { + let defaultProps: TaskItem["props"]; + + beforeEach(() => { + defaultProps = { + columnWidths: COLUMN_WIDTHS, + item: { ...TASK_MOCK, depends_on: ["task-id-2"] }, + otherItems: [ + { + ...TASK_MOCK, + id: "task-id-2", + }, + ], + open: true, + instancesDetails: [INSTANCE_MOCK], + onDependsOnClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(TASK_MOCK.id)).toBeTruthy(); + }); + + it("renders '-' for no progress update", () => { + const { getByText } = render( + + ); + expect(getByText("-")).toBeTruthy(); + }); + + it("renders progress update percentage", () => { + render( + + ); + const progressBar = TestUtils.select("ProgressBar__Progress"); + expect(progressBar).toBeTruthy(); + }); + + it("doesn't render progress bar if no percentage", () => { + render( + + ); + expect(TestUtils.select("ProgressBar__Progress")).toBeFalsy(); + }); + + it("copies exception to clipboard", () => { + jest.mock("@src/utils/DomUtils", () => ({ + getEventPath: jest.fn(), + })); + const copyTextToClipboard = jest + .spyOn(DomUtils, "copyTextToClipboard") + .mockImplementation(() => Promise.resolve(true)); + copyTextToClipboard; + render(); + const exceptionText = TestUtils.select("TaskItem__ExceptionText")!; + const copyButton = TestUtils.select("CopyButton__Wrapper", exceptionText); + exceptionText.click(); + copyButton?.click(); + fireEvent.mouseDown(exceptionText); + fireEvent.mouseUp(exceptionText); + expect(copyTextToClipboard).toHaveBeenCalledTimes(2); + }); + + it("fires dependsOn click", () => { + render(); + const dependsOnValueElement = TestUtils.select( + "TaskItem__Value", + TestUtils.select("TaskItem__DependsOnIds")! + )!; + fireEvent.mouseDown(dependsOnValueElement); + fireEvent.mouseUp(dependsOnValueElement); + dependsOnValueElement.click(); + + expect(defaultProps.onDependsOnClick).toHaveBeenCalledTimes(1); + }); + + it("render 'N/A' if no exception text", () => { + render( + + ); + expect( + Array.from(TestUtils.selectAll("TaskItem__Label-")).find( + el => el.textContent === "Exception Details" + )?.nextElementSibling?.textContent + ).toBe("N/A"); + }); +}); diff --git a/src/components/modules/TransferModule/TaskItem/TaskItem.tsx b/src/components/modules/TransferModule/TaskItem/TaskItem.tsx index 6eeb88da..ab92821e 100644 --- a/src/components/modules/TransferModule/TaskItem/TaskItem.tsx +++ b/src/components/modules/TransferModule/TaskItem/TaskItem.tsx @@ -335,9 +335,6 @@ class TaskItem extends React.Component { return ( {this.props.item.progress_updates.map((update, i) => { - if (!update) { - return N/A; - } const progressPercentage = this.getProgressPercentage(update); return ( diff --git a/src/components/modules/TransferModule/TaskItem/test.tsx b/src/components/modules/TransferModule/TaskItem/test.tsx deleted file mode 100644 index 2a316160..00000000 --- a/src/components/modules/TransferModule/TaskItem/test.tsx +++ /dev/null @@ -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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import TaskItem from "."; - -const wrap = props => new TW(shallow(), "taskItem"); - -const item = { - progress_updates: [ - { message: "the task has a progress of 50%", created_at: new Date() }, - { message: "the task is almost done", created_at: new Date() }, - ], - exception_details: "Exception details", - status: "RUNNING", - created_at: new Date(), - depends_on: ["depends on id"], - id: "item-id", - task_type: "Task name", -}; -const columnWidths = ["26%", "18%", "36%", "20%"]; - -describe("TaskItem Component", () => { - it("renders progress updates", () => { - const wrapper = wrap({ item, columnWidths, open: true }); - expect(wrapper.findText("progressUpdateMessage-1")).toBe( - "the task is almost done" - ); - }); - - it("renders progress bar", () => { - const wrapper = wrap({ item, columnWidths, open: true }); - expect(wrapper.find("progressBar-0").prop("progress")).toBe(50); - }); -}); diff --git a/src/components/modules/TransferModule/Tasks/Tasks.spec.tsx b/src/components/modules/TransferModule/Tasks/Tasks.spec.tsx new file mode 100644 index 00000000..9c9ea7db --- /dev/null +++ b/src/components/modules/TransferModule/Tasks/Tasks.spec.tsx @@ -0,0 +1,123 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { fireEvent, render } from "@testing-library/react"; +import { TASK_MOCK } from "@tests/mocks/ExecutionsMock"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; +import TestUtils from "@tests/TestUtils"; + +import Tasks from "./"; + +jest.mock("@src/components/modules/TransferModule/TaskItem", () => ({ + __esModule: true, + default: (props: any) => ( +
+
+ ID: {props.item.id}; Open: {String(props.open)}; +
+
{ + props.onDependsOnClick(props.item.depends_on[0]); + }} + /> +
+ ), +})); + +describe("Tasks", () => { + let defaultProps: Tasks["props"]; + + beforeEach(() => { + defaultProps = { + items: [ + { ...TASK_MOCK }, + { ...TASK_MOCK, id: "task-2", depends_on: ["task-id"] }, + ], + instancesDetails: [INSTANCE_MOCK], + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Task")).toBeTruthy(); + }); + + it("opens running task, closes when not running", () => { + const { rerender, getByTestId } = render(); + const taskItem = getByTestId("TaskItem-task-2"); + expect(taskItem).toBeTruthy(); + expect(taskItem.textContent).toContain("Open: false"); + + rerender( + + ); + expect(taskItem.textContent).toContain("Open: true"); + + rerender(); + expect(taskItem.textContent).toContain("Open: false"); + }); + + it("handles a little drag as mouse click", () => { + const { getByTestId } = render(); + const taskItem = getByTestId("TaskItem-task-id"); + expect(taskItem).toBeTruthy(); + expect(taskItem.textContent).toContain("Open: false"); + + fireEvent.mouseDown(taskItem); + fireEvent.mouseUp(taskItem); + expect(taskItem.textContent).toContain("Open: true"); + + fireEvent.mouseDown(taskItem); + fireEvent.mouseUp(taskItem); + expect(taskItem.textContent).toContain("Open: false"); + }); + + it("handles depends on click", () => { + const { getByTestId } = render(); + const firstTaskItem = getByTestId("TaskItem-task-id"); + const secondTaskItem = getByTestId("TaskItem-task-2"); + expect(firstTaskItem).toBeTruthy(); + expect(secondTaskItem).toBeTruthy(); + expect(firstTaskItem.textContent).toContain("Open: false"); + expect(secondTaskItem.textContent).toContain("Open: false"); + + fireEvent.mouseDown(secondTaskItem); + fireEvent.mouseUp(secondTaskItem); + expect(secondTaskItem.textContent).toContain("Open: true"); + const dependsOn = secondTaskItem.querySelector( + "[data-testid='TaskItem-DependsOn']" + ) as HTMLElement; + expect(dependsOn).toBeTruthy(); + dependsOn!.click(); + expect(firstTaskItem.textContent).toContain("Open: true"); + }); + + it("renders loading", () => { + render(); + expect(TestUtils.select("Tasks__LoadingWrapper")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/TransferModule/Tasks/Tasks.tsx b/src/components/modules/TransferModule/Tasks/Tasks.tsx index 04bd0655..7ebe415a 100644 --- a/src/components/modules/TransferModule/Tasks/Tasks.tsx +++ b/src/components/modules/TransferModule/Tasks/Tasks.tsx @@ -12,18 +12,17 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled from "styled-components"; +import { Instance } from "@src/@types/Instance"; import TaskItem from "@src/components/modules/TransferModule/TaskItem"; - -import type { Task } from "@src/@types/Task"; import { ThemePalette, ThemeProps } from "@src/components/Theme"; import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; -import { Instance } from "@src/@types/Instance"; -const ColumnWidths = ["26%", "18%", "36%", "20%"]; +import type { Task } from "@src/@types/Task"; +const COLUMN_WIDTHS = ["26%", "18%", "36%", "20%"]; const Wrapper = styled.div``; const ContentWrapper = styled.div` @@ -145,10 +144,10 @@ class Tasks extends React.Component { renderHeader() { return (
- Task - Instance - Latest Message - Timestamp + Task + Instance + Latest Message + Timestamp
); } @@ -164,7 +163,7 @@ class Tasks extends React.Component { item={item} otherItems={this.props.items.filter(i => i.id !== item.id)} instancesDetails={this.props.instancesDetails} - columnWidths={ColumnWidths} + columnWidths={COLUMN_WIDTHS} open={Boolean(this.state.openedItems.find(i => i.id === item.id))} onDependsOnClick={id => { this.handleDependsOnClick(id); diff --git a/src/components/modules/TransferModule/Tasks/test.tsx b/src/components/modules/TransferModule/Tasks/test.tsx deleted file mode 100644 index 7541774f..00000000 --- a/src/components/modules/TransferModule/Tasks/test.tsx +++ /dev/null @@ -1,109 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import Tasks from "."; - -const wrap = props => new TW(shallow(), "tasks"); - -const items = [ - { - progress_updates: [ - { message: "the task has a progress of 10%", created_at: new Date() }, - ], - exception_details: "Exception details", - status: "COMPLETED", - created_at: new Date(), - depends_on: ["depends on id"], - id: "task-1", - task_type: "Task name 1", - }, - { - progress_updates: [ - { message: "the task has a progress of 50%", created_at: new Date() }, - { message: "the task is almost done", created_at: new Date() }, - ], - exception_details: "Exception details", - status: "CANCELED", - created_at: new Date(), - depends_on: ["depends on id"], - id: "task-2", - task_type: "Task name 2", - }, - { - progress_updates: [ - { message: "the task has a progress of 50%", created_at: new Date() }, - { message: "the task is almost done", created_at: new Date() }, - ], - exception_details: "Exception details", - status: "ERROR", - created_at: new Date(), - depends_on: ["depends on id"], - id: "task-3", - task_type: "Task name 3", - }, - { - progress_updates: [ - { message: "the task has a progress of 50%", created_at: new Date() }, - { message: "the task is almost done", created_at: new Date() }, - ], - exception_details: "Exception details", - status: "RUNNING", - created_at: new Date(), - depends_on: ["depends on id"], - id: "task-4", - task_type: "Task name 4", - }, - { - progress_updates: [ - { message: "the task has a progress of 50%", created_at: new Date() }, - { message: "the task is almost done", created_at: new Date() }, - ], - exception_details: "Exception details", - status: "PENDING", - created_at: new Date(), - depends_on: ["depends on id"], - id: "task-5", - task_type: "Task name 5", - }, -]; - -describe("Tasks Component", () => { - it("renders correct number of task items", () => { - const wrapper = wrap({ items }); - items.forEach(item => { - expect(wrapper.find(`item-${item.id}`).prop("item").id).toBe(item.id); - }); - }); - - it("renders only running task opened", () => { - const wrapper = wrap({ items }); - expect(wrapper.find("item-task-1").prop("open")).toBe(false); - expect(wrapper.find("item-task-2").prop("open")).toBe(false); - expect(wrapper.find("item-task-3").prop("open")).toBe(false); - expect(wrapper.find("item-task-4").prop("open")).toBe(true); - expect(wrapper.find("item-task-5").prop("open")).toBe(false); - }); - - it("renders correct info in task item", () => { - const wrapper = wrap({ items }); - expect(wrapper.find("item-task-3").prop("item").id).toBe("task-3"); - expect(wrapper.find("item-task-5").prop("item").task_type).toBe( - "Task name 5" - ); - expect(wrapper.find("item-task-1").prop("item").status).toBe("COMPLETED"); - }); -}); diff --git a/src/components/modules/TransferModule/Timeline/test.tsx b/src/components/modules/TransferModule/Timeline/test.tsx deleted file mode 100644 index f629e565..00000000 --- a/src/components/modules/TransferModule/Timeline/test.tsx +++ /dev/null @@ -1,62 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import moment from "moment"; -import TW from "@src/utils/TestWrapper"; -import Timeline from "."; - -const wrap = props => new TW(shallow(), "timeline"); - -const items = [ - { id: "item-1", status: "ERROR", created_at: new Date(2017, 1, 2) }, - { id: "item-2", status: "COMPLETED", created_at: new Date(2017, 2, 3) }, - { id: "item-3", status: "RUNNING", created_at: new Date(2017, 3, 4) }, -]; - -describe("Timeline Component", () => { - it("renders with correct dates", () => { - const wrapper = wrap({ items, selectedItem: items[2] }); - expect(wrapper.findPartialId("label-").length).toBe(items.length); - items.forEach(item => { - expect(wrapper.findText(`label-${item.id}`)).toBe( - moment(item.created_at).format("DD MMM YYYY") - ); - }); - }); - - it("dispatches item click", () => { - const onItemClick = sinon.spy(); - const wrapper = wrap({ items, selectedItem: items[2], onItemClick }); - wrapper.find(`item-${items[1].id}`).simulate("click"); - expect(onItemClick.args[0][0].id).toBe("item-2"); - }); - - it("dispatches next and previous click", () => { - const onPreviousClick = sinon.spy(); - const onNextClick = sinon.spy(); - const wrapper = wrap({ - items, - selectedItem: items[2], - onPreviousClick, - onNextClick, - }); - wrapper.find("previous").simulate("click"); - wrapper.find("next").simulate("click"); - expect(onPreviousClick.calledOnce).toBe(true); - expect(onNextClick.calledOnce).toBe(true); - }); -}); diff --git a/src/components/modules/TransferModule/TransferDetailsTable/TransferDetailsTable.spec.tsx b/src/components/modules/TransferModule/TransferDetailsTable/TransferDetailsTable.spec.tsx new file mode 100644 index 00000000..9ef58b3f --- /dev/null +++ b/src/components/modules/TransferModule/TransferDetailsTable/TransferDetailsTable.spec.tsx @@ -0,0 +1,91 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; +import { MINION_POOL_MOCK } from "@tests/mocks/MinionPoolMock"; +import { NETWORK_MOCK } from "@tests/mocks/NetworksMock"; +import { STORAGE_BACKEND_MOCK } from "@tests/mocks/StoragesMock"; +import { REPLICA_MOCK } from "@tests/mocks/TransferMock"; +import TestUtils from "@tests/TestUtils"; + +import TransferDetailsTable from "./"; + +describe("TransferDetailsTable", () => { + let defaultProps: TransferDetailsTable["props"]; + + beforeEach(() => { + defaultProps = { + item: REPLICA_MOCK, + instancesDetails: [INSTANCE_MOCK], + networks: [NETWORK_MOCK], + minionPools: [MINION_POOL_MOCK], + storageBackends: [STORAGE_BACKEND_MOCK], + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Source")).toBeTruthy(); + expect(getByText("Target")).toBeTruthy(); + }); + + it("renders without crashing when no transfer result and no disabled disks", () => { + const { getByText } = render( + + ); + expect(getByText("Source")).toBeTruthy(); + expect(getByText("Target")).toBeTruthy(); + }); + + it("handles row click", () => { + render(); + const rows = TestUtils.selectAll("TransferDetailsTable__Row-"); + expect(rows[0]).toBeTruthy(); + expect(rows[1]).toBeTruthy(); + const firstArrow = () => TestUtils.select("Arrow__Wrapper-", rows[0])!; + const secondArrow = () => TestUtils.select("Arrow__Wrapper-", rows[1])!; + expect(firstArrow()).toBeTruthy(); + expect(secondArrow()).toBeTruthy(); + + expect(firstArrow().getAttribute("orientation")).toBe("down"); + rows[0].click(); + expect(firstArrow().getAttribute("orientation")).toBe("up"); + + expect(secondArrow().getAttribute("orientation")).toBe("down"); + rows[1].click(); + expect(secondArrow().getAttribute("orientation")).toBe("up"); + rows[1].click(); + expect(secondArrow().getAttribute("orientation")).toBe("down"); + }); +}); diff --git a/src/components/modules/TransferModule/TransferDetailsTable/TransferDetailsTable.tsx b/src/components/modules/TransferModule/TransferDetailsTable/TransferDetailsTable.tsx index b8c8302d..9f1ca9f7 100644 --- a/src/components/modules/TransferModule/TransferDetailsTable/TransferDetailsTable.tsx +++ b/src/components/modules/TransferModule/TransferDetailsTable/TransferDetailsTable.tsx @@ -154,7 +154,6 @@ export const ArrowIcon = styled.div` background: url("${arrowIcon}") center no-repeat; margin-left: 16px; `; -export const TEST_ID = "mainDetailsTable"; export type Props = { item?: TransferItem | null; diff --git a/src/components/modules/TransferModule/TransferDetailsTable/test.tsx b/src/components/modules/TransferModule/TransferDetailsTable/test.tsx deleted file mode 100644 index f5a86030..00000000 --- a/src/components/modules/TransferModule/TransferDetailsTable/test.tsx +++ /dev/null @@ -1,120 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; - -import type { MainItem } from "@src/@types/MainItem"; -import type { Instance } from "@src/@types/Instance"; -import TW from "@src/utils/TestWrapper"; -import Component, { TEST_ID } from "./TransferDetailsTable"; -import type { Props } from "./TransferDetailsTable"; - -const defaultInstance: Instance = { - id: "instance1id", - name: "instance1name", - flavor_name: "instance1flavorname", - instance_name: "instance1instancename", - num_cpu: 2, - memory_mb: 2048, - os_type: "windows", - devices: { - nics: [ - { - id: "instance1nic1", - network_name: "network1", - mac_address: "instance1macaddress", - network_id: "network1", - }, - ], - disks: [ - { - id: "instance1disk1", - name: "instance1disk1name", - storage_backend_identifier: "asdaf", - }, - ], - }, -}; - -const defaultItem: MainItem = { - id: "id", - executions: [], - name: "name", - notes: "notes", - status: "COMPLETED", - tasks: [], - created_at: new Date(2019, 2, 18, 13, 19, 10), - updated_at: new Date(2019, 2, 19, 14, 18, 55), - origin_endpoint_id: "origin", - destination_endpoint_id: "destination", - instances: ["instance1"], - type: "replica", - info: { - instance1: { - export_info: { - devices: { - nics: [{ network_name: "network1" }, { network_name: "network2" }], - }, - }, - }, - }, - destination_environment: { option1: "value1" }, - source_environment: { option1: "value1" }, - transfer_result: { - instance1: defaultInstance, - }, - storage_mappings: { - backend_mappings: [ - { - destination: "asdaf", - source: "asdaf1", - }, - ], - default: "asdaf", - disk_mappings: [ - { - destination: "asdaf", - disk_id: "instance1disk1", - }, - ], - }, - network_map: { - network1: "network2", - }, -}; - -const defaultProps: Props = { - item: defaultItem, - instancesDetails: [defaultInstance], -}; -const wrap = (props: Props) => - new TW(shallow(), TEST_ID); - -describe("MainDetailsTable Component", () => { - it("renders basic info", () => { - const wrapper = wrap(defaultProps); - defaultProps.instancesDetails.forEach(i => { - expect(wrapper.findText(`instanceName-${i.name}`)).toBe(i.name); - }); - expect(wrapper.findText("source-instance")).toBe("instance1"); - expect(wrapper.findText("destination-instance")).toBe("instance1"); - - expect(wrapper.findText("source-network")).toBe("instance1macaddress"); - expect(wrapper.findText("destination-network")).toBe("network2"); - - expect(wrapper.findText("source-storage")).toBe("instance1disk1"); - expect(wrapper.findText("destination-storage")).toBe("instance1disk1name"); - }); -}); diff --git a/src/components/modules/TransferModule/TransferListItem/TransferListItem.spec.tsx b/src/components/modules/TransferModule/TransferListItem/TransferListItem.spec.tsx new file mode 100644 index 00000000..1663bcc0 --- /dev/null +++ b/src/components/modules/TransferModule/TransferListItem/TransferListItem.spec.tsx @@ -0,0 +1,42 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; + +import TransferListItem from "."; +import { REPLICA_MOCK } from "@tests/mocks/TransferMock"; + +describe("TransferListItem", () => { + let defaultProps: TransferListItem["props"]; + + beforeEach(() => { + defaultProps = { + item: REPLICA_MOCK, + selected: false, + image: "image", + userNameLoading: false, + onSelectedChange: jest.fn(), + endpointType: jest.fn(), + getUserName: jest.fn(), + onClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(REPLICA_MOCK.notes!)).toBeTruthy(); + }); +}); diff --git a/src/components/modules/TransferModule/TransferListItem/test.tsx b/src/components/modules/TransferModule/TransferListItem/test.tsx deleted file mode 100644 index 88d1d974..00000000 --- a/src/components/modules/TransferModule/TransferListItem/test.tsx +++ /dev/null @@ -1,61 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TestWrapper from "@src/utils/TestWrapper"; -import MainListItem from "."; - -const wrap = props => - new TestWrapper(shallow(), "mainListItem"); - -const item = { - origin_endpoint_id: "openstack", - destination_endpoint_id: "azure", - instances: ["instance name"], - executions: [{ status: "COMPLETED", created_at: new Date() }], -}; -const endpointType = id => id; - -describe("MainListItem Component", () => { - it("renders with given status", () => { - const wrapper = wrap({ item, endpointType }); - expect(wrapper.findPartialId("statusPill").at(0).prop("status")).toBe( - "COMPLETED" - ); - }); - - it("renders with given endpoints", () => { - const wrapper = wrap({ item, endpointType }); - expect(wrapper.find("sourceLogo").prop("endpoint")).toBe( - item.origin_endpoint_id - ); - expect(wrapper.find("destLogo").prop("endpoint")).toBe( - item.destination_endpoint_id - ); - }); - - it("renders with selected", () => { - const wrapper = wrap({ item, endpointType, selected: true }); - expect(wrapper.find("checkbox").prop("checked")).toBe(true); - }); - - it("dispatched item click", () => { - const onClick = sinon.spy(); - const wrapper = wrap({ item, endpointType, onClick }); - wrapper.find("content").simulate("click"); - expect(onClick.calledOnce).toBe(true); - }); -}); diff --git a/src/components/modules/UserModule/UserDetailsContent/UserDetailsContent.spec.tsx b/src/components/modules/UserModule/UserDetailsContent/UserDetailsContent.spec.tsx new file mode 100644 index 00000000..f8a76dda --- /dev/null +++ b/src/components/modules/UserModule/UserDetailsContent/UserDetailsContent.spec.tsx @@ -0,0 +1,65 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { USER_MOCK } from "@tests/mocks/UsersMock"; +import TestUtils from "@tests/TestUtils"; + +import UserDetailsContent from "./"; + +jest.mock("react-router-dom", () => ({ Link: "a" })); + +describe("UserDetailsContent", () => { + let defaultProps: UserDetailsContent["props"]; + + beforeEach(() => { + defaultProps = { + user: USER_MOCK, + loading: false, + projects: [USER_MOCK.project], + userProjects: [USER_MOCK.project], + isLoggedUser: false, + onUpdatePasswordClick: jest.fn(), + onDeleteClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(USER_MOCK.name)).toBeTruthy(); + }); + + it("renders loading", () => { + render(); + expect(TestUtils.select("UserDetailsContent__LoadingWrapper")).toBeTruthy(); + }); + + it("fires delete click", () => { + const { getByText } = render(); + getByText("Delete user").click(); + expect(defaultProps.onDeleteClick).toHaveBeenCalled(); + }); + + it("renders without crashing when user projects are not in projects", () => { + const { getByText } = render( + + ); + expect(getByText(USER_MOCK.name)).toBeTruthy(); + }); +}); diff --git a/src/components/modules/UserModule/UserDetailsContent/UserDetailsContent.tsx b/src/components/modules/UserModule/UserDetailsContent/UserDetailsContent.tsx index df6ac19a..8ef3c736 100644 --- a/src/components/modules/UserModule/UserDetailsContent/UserDetailsContent.tsx +++ b/src/components/modules/UserModule/UserDetailsContent/UserDetailsContent.tsx @@ -12,20 +12,19 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +import { observer } from "mobx-react"; import React from "react"; import { Link } from "react-router-dom"; -import { observer } from "mobx-react"; import styled from "styled-components"; -import CopyValue from "@src/components/ui/CopyValue"; +import { ThemePalette, ThemeProps } from "@src/components/Theme"; +import Button from "@src/components/ui/Button"; import CopyMultilineValue from "@src/components/ui/CopyMultilineValue"; +import CopyValue from "@src/components/ui/CopyValue"; import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; -import Button from "@src/components/ui/Button"; import type { User } from "@src/@types/User"; import type { Project } from "@src/@types/Project"; -import { ThemePalette, ThemeProps } from "@src/components/Theme"; - const Wrapper = styled.div` ${ThemeProps.exactWidth(ThemeProps.contentWidth)} margin: 0 auto; @@ -77,8 +76,6 @@ const ButtonsColumn = styled.div` } `; -export const TEST_ID = "userDetailsContent"; - export type Props = { user: User | null; loading: boolean; diff --git a/src/components/modules/UserModule/UserDetailsContent/test.tsx b/src/components/modules/UserModule/UserDetailsContent/test.tsx deleted file mode 100644 index efa21a9c..00000000 --- a/src/components/modules/UserModule/UserDetailsContent/test.tsx +++ /dev/null @@ -1,105 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; - -import type { User } from "@src/@types/User"; -import type { Project } from "@src/@types/Project"; -import TW from "@src/utils/TestWrapper"; -import Component, { TEST_ID } from "."; -import type { Props } from "."; - -const anotherProject: Project = { - id: "project2-id", - name: "project2-name", - enabled: false, - description: "project2-description", -}; - -const defaultProject: Project = { - id: "project-id", - name: "project-name", - enabled: true, - description: "project-description", -}; - -const defaultUser: User = { - project: defaultProject, - email: "user@email.com", - name: "name", - id: "id", - description: "description", - enabled: true, - project_id: "project-id", - domain_id: "default", - isAdmin: true, - password: "password", -}; -const defaultProps: Props = { - user: defaultUser, - loading: false, - projects: [defaultProject, anotherProject], - userProjects: [defaultProject, anotherProject], - isLoggedUser: true, - onDeleteClick: () => {}, - onUpdatePasswordClick: () => {}, -}; -const wrap = (props: Props) => - new TW(shallow(), TEST_ID); - -describe("UserDetailsContent Component", () => { - it("renders info", () => { - const wrapper = wrap(defaultProps); - expect(wrapper.find("name").prop("value")).toBe(defaultUser.name); - expect(wrapper.find("id").prop("value")).toBe(defaultUser.id); - expect(wrapper.find("email").prop("value")).toBe(defaultUser.email); - expect(wrapper.find("primaryProject").prop("value")).toBe( - defaultProject.name - ); - expect(wrapper.findText("enabled")).toBe("Yes"); - }); - - it("renders project membership", () => { - const wrapper = wrap(defaultProps); - expect(wrapper.find("project-project-id").prop("to")).toBe( - "/project/project-id" - ); - expect(wrapper.find("project-project2-id").prop("to")).toBe( - "/project/project2-id" - ); - }); - - it("dispatches delete an update clicks", () => { - const newProps: Props = { ...defaultProps }; - newProps.onDeleteClick = sinon.spy(); - newProps.onUpdatePasswordClick = sinon.spy(); - const wrapper = wrap(newProps); - const deleteButton = wrapper.find("deleteUserButton"); - deleteButton.simulate("click"); - const updateButton = wrapper.find("updateButton"); - updateButton.simulate("click"); - - expect(newProps.onDeleteClick.called).toBe(true); - expect(newProps.onUpdatePasswordClick.called).toBe(true); - }); - - it("has delete disabled if is the logged in user", () => { - let wrapper = wrap(defaultProps); - expect(wrapper.find("deleteUserButton").prop("disabled")).toBe(true); - wrapper = wrap({ ...defaultProps, isLoggedUser: false }); - expect(wrapper.find("deleteUserButton").prop("disabled")).toBe(false); - }); -}); diff --git a/src/components/modules/UserModule/UserListItem/UserListItem.spec.tsx b/src/components/modules/UserModule/UserListItem/UserListItem.spec.tsx new file mode 100644 index 00000000..091e5c31 --- /dev/null +++ b/src/components/modules/UserModule/UserListItem/UserListItem.spec.tsx @@ -0,0 +1,37 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { USER_MOCK } from "@tests/mocks/UsersMock"; + +import UserListItem from "./"; + +describe("UserListItem", () => { + let defaultProps: UserListItem["props"]; + + beforeEach(() => { + defaultProps = { + item: USER_MOCK, + onClick: jest.fn(), + getProjectName: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(USER_MOCK.name)).toBeTruthy(); + }); +}); diff --git a/src/components/modules/UserModule/UserListItem/UserListItem.tsx b/src/components/modules/UserModule/UserListItem/UserListItem.tsx index f2764a91..7914f6da 100644 --- a/src/components/modules/UserModule/UserListItem/UserListItem.tsx +++ b/src/components/modules/UserModule/UserListItem/UserListItem.tsx @@ -12,15 +12,15 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +import { observer } from "mobx-react"; import React from "react"; import styled from "styled-components"; -import { observer } from "mobx-react"; -import type { User } from "@src/@types/User"; import { ThemePalette, ThemeProps } from "@src/components/Theme"; import userImage from "./images/user.svg"; +import type { User } from "@src/@types/User"; const Content = styled.div` display: flex; align-items: center; diff --git a/src/components/modules/UserModule/UserListItem/test.tsx b/src/components/modules/UserModule/UserListItem/test.tsx deleted file mode 100644 index 17001e45..00000000 --- a/src/components/modules/UserModule/UserListItem/test.tsx +++ /dev/null @@ -1,60 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import type { User } from "@src/@types/User"; -import UserListItem from "."; - -type Props = { - item: User; - onClick: () => void; - getProjectName: (projectId: string | null) => string; -}; - -const wrap = (props: Props) => - new TW(shallow(), "ulItem"); - -const user = { - id: "id", - name: "User Name", - description: "user description", - email: "user@email.com", - project_id: "project_id", - enabled: true, - project: { name: "", id: "" }, -}; -describe("UserListItem Component", () => { - it("renders with correct data", () => { - const wrapper = wrap({ - item: user, - onClick: () => {}, - getProjectName: id => `project ${id || ""}`, - }); - expect(wrapper.findText("name")).toBe(user.name); - expect(wrapper.findText("description")).toBe(user.description); - expect(wrapper.findText("email")).toBe(user.email); - expect(wrapper.findText("project")).toBe("project project_id"); - expect(wrapper.findText("enabled")).toBe("Yes"); - }); - - it("dispatches click", () => { - const onClick = sinon.spy(); - const wrapper = wrap({ item: user, onClick, getProjectName: () => "" }); - wrapper.find("content").click(); - expect(onClick.calledOnce).toBe(true); - }); -}); diff --git a/src/components/modules/UserModule/UserModal/test.tsx b/src/components/modules/UserModule/UserModal/test.tsx deleted file mode 100644 index b8b90477..00000000 --- a/src/components/modules/UserModule/UserModal/test.tsx +++ /dev/null @@ -1,123 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import type { Project } from "@src/@types/Project"; -import type { User } from "@src/@types/User"; -import UserModal from "."; - -type Props = { - user?: User; - isLoggedUser?: boolean; - loading: boolean; - isNewUser?: boolean; - projects: Project[]; - editPassword?: boolean; - onRequestClose: () => void; - onUpdateClick: (user: User) => void; -}; - -const wrap = (props: Props) => - new TW(shallow(), "userModal"); -const projects: Project[] = [ - { id: "project-1", name: "Project 1" }, - { id: "project-2", name: "Project 2" }, -]; -describe("UserModal Component", () => { - it("doesn't dispatch click if required fields are not filled", () => { - const onUpdateClick = sinon.spy(); - const wrapper = wrap({ - isNewUser: true, - isLoggedUser: false, - loading: false, - projects, - onRequestClose: () => {}, - onUpdateClick, - }); - expect(wrapper.findText("updateButton", false, true)).toBe("New User"); - wrapper.find("updateButton").click(); - expect(onUpdateClick.called).toBe(false); - expect(wrapper.find("field-username").prop("highlight")).toBe(true); - expect(wrapper.find("field-new_password").prop("highlight")).toBe(true); - }); - - it("dispatches click if project is filled", () => { - const onUpdateClick = sinon.spy(); - const wrapper = wrap({ - user: { - id: "user-1", - name: "User 1", - email: "email", - project: projects[0], - }, - isNewUser: false, - isLoggedUser: false, - loading: false, - projects, - onRequestClose: () => {}, - onUpdateClick, - }); - expect(wrapper.findText("updateButton", false, true)).toBe("Update User"); - wrapper.find("updateButton").click(); - expect(onUpdateClick.called).toBe(true); - }); - - it("has disabled fields on loading", () => { - const wrapper = wrap({ - user: { - id: "user-1", - name: "User 1", - email: "email", - project: projects[0], - }, - isNewUser: false, - isLoggedUser: false, - loading: true, - projects, - onRequestClose: () => {}, - onUpdateClick: () => {}, - }); - expect(wrapper.find("updateButton").prop("disabled")).toBe(true); - expect(wrapper.find("field-username").prop("disabled")).toBe(true); - expect(wrapper.find("field-new_password").length).toBe(0); - expect(wrapper.find("field-confirm_password").length).toBe(0); - }); - - it("renders change password form", () => { - const wrapper = wrap({ - user: { - id: "user-1", - name: "User 1", - email: "email", - project: projects[0], - }, - isNewUser: false, - isLoggedUser: false, - loading: true, - projects, - editPassword: true, - onRequestClose: () => {}, - onUpdateClick: () => {}, - }); - - expect(wrapper.findText("updateButton", false, true)).toBe( - "Change Password" - ); - expect(wrapper.find("field-new_password").length).toBe(1); - expect(wrapper.find("field-confirm_password").length).toBe(1); - }); -}); diff --git a/src/components/modules/WizardModule/WizardBreadcrumbs/test.tsx b/src/components/modules/WizardModule/WizardBreadcrumbs/WizardBreadcrumbs.spec.tsx similarity index 52% rename from src/components/modules/WizardModule/WizardBreadcrumbs/test.tsx rename to src/components/modules/WizardModule/WizardBreadcrumbs/WizardBreadcrumbs.spec.tsx index 517b3551..71f00629 100644 --- a/src/components/modules/WizardModule/WizardBreadcrumbs/test.tsx +++ b/src/components/modules/WizardModule/WizardBreadcrumbs/WizardBreadcrumbs.spec.tsx @@ -1,5 +1,5 @@ /* -Copyright (C) 2017 Cloudbase Solutions SRL +Copyright (C) 2023 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,29 +13,26 @@ along with this program. If not, see . */ import React from "react"; -import { shallow } from "enzyme"; + +import { render } from "@testing-library/react"; + import WizardBreadcrumbs from "."; -import TW from "@src/utils/TestWrapper"; -import { wizardPages } from "@src/constants"; -const wrap = props => - new TW( - shallow( - - ), - "wBreadCrumbs" - ); +describe("WizardBreadcrumbs", () => { + let defaultProps: WizardBreadcrumbs["props"]; + + beforeEach(() => { + defaultProps = { + selected: { id: "2" }, + pages: [ + { id: "1", title: "The title 1", breadcrumb: "The breadcrumb 1" }, + { id: "2", title: "The title 2", breadcrumb: "The breadcrumb 2" }, + ], + }; + }); -describe("WizardBreadcrumbs Component", () => { - it("has correct page selected", () => { - const wrapper = wrap({ selected: wizardPages[3] }); - expect(wrapper.findText(`name-${wizardPages[3].id}`)).toBe( - wizardPages[3].breadcrumb - ); + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("The breadcrumb 1")).toBeTruthy(); }); }); diff --git a/src/components/modules/WizardModule/WizardBreadcrumbs/WizardBreadcrumbs.tsx b/src/components/modules/WizardModule/WizardBreadcrumbs/WizardBreadcrumbs.tsx index 53e35253..569841d1 100644 --- a/src/components/modules/WizardModule/WizardBreadcrumbs/WizardBreadcrumbs.tsx +++ b/src/components/modules/WizardModule/WizardBreadcrumbs/WizardBreadcrumbs.tsx @@ -12,13 +12,13 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled from "styled-components"; +import { ThemePalette } from "@src/components/Theme"; import Arrow from "@src/components/ui/Arrow"; -import { ThemePalette } from "@src/components/Theme"; import type { WizardPage } from "@src/@types/WizardData"; const Wrapper = styled.div` diff --git a/src/components/modules/WizardModule/WizardEndpointList/WizardEndpointList.spec.tsx b/src/components/modules/WizardModule/WizardEndpointList/WizardEndpointList.spec.tsx new file mode 100644 index 00000000..ef600b6e --- /dev/null +++ b/src/components/modules/WizardModule/WizardEndpointList/WizardEndpointList.spec.tsx @@ -0,0 +1,48 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { + OPENSTACK_ENDPOINT_MOCK, + VMWARE_ENDPOINT_MOCK, +} from "@tests/mocks/EndpointsMock"; + +import WizardEndpointList from "./"; + +jest.mock("@src/components/modules/EndpointModule/EndpointLogos", () => ({ + __esModule: true, + default: (props: any) =>
{props.endpoint}
, +})); + +describe("WizardEndpointList", () => { + let defaultProps: WizardEndpointList["props"]; + + beforeEach(() => { + defaultProps = { + providers: ["vmware_vsphere", "openstack"], + endpoints: [VMWARE_ENDPOINT_MOCK, OPENSTACK_ENDPOINT_MOCK], + loading: false, + onChange: jest.fn(), + onAddEndpoint: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("vmware_vsphere")).toBeTruthy(); + expect(getByText("openstack")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/WizardModule/WizardEndpointList/WizardEndpointList.tsx b/src/components/modules/WizardModule/WizardEndpointList/WizardEndpointList.tsx index 9793cb77..6f7ed6b3 100644 --- a/src/components/modules/WizardModule/WizardEndpointList/WizardEndpointList.tsx +++ b/src/components/modules/WizardModule/WizardEndpointList/WizardEndpointList.tsx @@ -12,18 +12,17 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled, { css } from "styled-components"; +import { ProviderTypes } from "@src/@types/Providers"; import EndpointLogos from "@src/components/modules/EndpointModule/EndpointLogos"; +import Button from "@src/components/ui/Button"; import Dropdown from "@src/components/ui/Dropdowns/Dropdown"; import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; -import Button from "@src/components/ui/Button"; import type { Endpoint } from "@src/@types/Endpoint"; -import { ProviderTypes } from "@src/@types/Providers"; - const Wrapper = styled.div` display: flex; flex-direction: column; diff --git a/src/components/modules/WizardModule/WizardEndpointList/test.tsx b/src/components/modules/WizardModule/WizardEndpointList/test.tsx deleted file mode 100644 index 8aebe826..00000000 --- a/src/components/modules/WizardModule/WizardEndpointList/test.tsx +++ /dev/null @@ -1,102 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import WizardEndpointList from "."; - -const wrap = props => - new TW(shallow(), "wEndpointList"); - -const providers = [ - "openstack", - "azure", - "aws", - "opc", - "oracle_vm", - "vmware_vsphere", -]; - -const endpoints = [ - { id: "e-1", name: "An endpoint", type: "openstack" }, - { id: "e-2", name: "Another endpoint", type: "azure" }, - { id: "e-3", name: "Yet another endpoint", type: "azure" }, -]; - -describe("WizardEndpointList Component", () => { - it("renders correct number of providers", () => { - const wrapper = wrap({ endpoints, providers }); - expect(wrapper.findPartialId("logo-").length).toBe(providers.length); - }); - - it("renders correct providers type", () => { - const wrapper = wrap({ endpoints, providers }); - expect(wrapper.find(`logo-${providers[0]}`).prop("endpoint")).toBe( - providers[0] - ); - expect(wrapper.find(`logo-${providers[1]}`).prop("endpoint")).toBe( - providers[1] - ); - expect(wrapper.find(`logo-${providers[2]}`).prop("endpoint")).toBe( - providers[2] - ); - expect(wrapper.find(`logo-${providers[3]}`).prop("endpoint")).toBe( - providers[3] - ); - expect(wrapper.find(`logo-${providers[4]}`).prop("endpoint")).toBe( - providers[4] - ); - expect(wrapper.find(`logo-${providers[5]}`).prop("endpoint")).toBe( - providers[5] - ); - }); - - it("has providers with correct enpoints available", () => { - const wrapper = wrap({ endpoints, providers }); - expect(wrapper.find("dropdown-openstack").prop("items").length).toBe(2); - expect(wrapper.find("dropdown-openstack").prop("items")[0].id).toBe("e-1"); - expect(wrapper.find("dropdown-azure").prop("items").length).toBe(3); - expect(wrapper.find("dropdown-azure").prop("items")[0].id).toBe("e-2"); - expect(wrapper.find("dropdown-azure").prop("items")[1].id).toBe("e-3"); - }); - - it("renders add new", () => { - const wrapper = wrap({ endpoints, providers }); - expect(wrapper.find("addButton-opc").length).toBe(1); - expect(wrapper.find("addButton-aws").length).toBe(1); - expect(wrapper.find("addButton-oracle_vm").length).toBe(1); - expect(wrapper.find("addButton-vmware_vsphere").length).toBe(1); - expect(wrapper.find("addButton-openstack").length).toBe(0); - expect(wrapper.find("addButton-azure").length).toBe(0); - }); - - // it('renders loading', () => { - // let wrapper = wrap({ endpoints, providers, loading: true }) - // expect(wrapper.find('StatusImage').prop('loading')).toBe(true) - // }) - - // it('renders dropdown as primary if endpoint is selected', () => { - // let wrapper = wrap({ endpoints, providers, selectedEndpoint: { ...endpoints[1] } }) - // expect(wrapper.find('Dropdown').at(1).prop('primary')).toBe(true) - // expect(wrapper.find('Dropdown').at(0).prop('primary')).toBe(false) - // }) - - // it('doesn\'t render endpoint if another endpoint is supplied', () => { - // let wrapper = wrap({ endpoints, providers, otherEndpoint: { ...endpoints[1] } }) - // expect(wrapper.find('Dropdown').at(1).prop('items').length).toBe(2) - // expect(wrapper.find('Dropdown').at(1).prop('items')[0].id).toBe('e-3') - // expect(wrapper.find('Dropdown').at(1).prop('items')[1].id).toBe('addNew') - // }) -}); diff --git a/src/components/modules/WizardModule/WizardInstances/WizardInstances.spec.tsx b/src/components/modules/WizardModule/WizardInstances/WizardInstances.spec.tsx new file mode 100644 index 00000000..f236f893 --- /dev/null +++ b/src/components/modules/WizardModule/WizardInstances/WizardInstances.spec.tsx @@ -0,0 +1,48 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; + +import WizardInstances from "."; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; + +describe("WizardInstances", () => { + let defaultProps: WizardInstances["props"]; + + beforeEach(() => { + defaultProps = { + instances: [INSTANCE_MOCK], + currentPage: 1, + instancesPerPage: 10, + loading: false, + chunksLoading: false, + searching: false, + searchNotFound: false, + reloading: false, + hasSourceOptions: true, + onSearchInputChange: jest.fn(), + onReloadClick: jest.fn(), + onInstanceClick: jest.fn(), + onPageClick: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(INSTANCE_MOCK.name)).toBeTruthy(); + expect(getByText(INSTANCE_MOCK.instance_name!)).toBeTruthy(); + }); +}); diff --git a/src/components/modules/WizardModule/WizardInstances/WizardInstances.tsx b/src/components/modules/WizardModule/WizardInstances/WizardInstances.tsx index 69685002..88323a4d 100644 --- a/src/components/modules/WizardModule/WizardInstances/WizardInstances.tsx +++ b/src/components/modules/WizardModule/WizardInstances/WizardInstances.tsx @@ -12,25 +12,25 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled from "styled-components"; -import Checkbox from "@src/components/ui/Checkbox"; -import ReloadButton from "@src/components/ui/ReloadButton"; -import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; +import { ThemePalette, ThemeProps } from "@src/components/Theme"; import Button from "@src/components/ui/Button"; -import SearchInput from "@src/components/ui/SearchInput"; +import Checkbox from "@src/components/ui/Checkbox"; import InfoIcon from "@src/components/ui/InfoIcon"; import Pagination from "@src/components/ui/Pagination"; +import ReloadButton from "@src/components/ui/ReloadButton"; +import SearchInput from "@src/components/ui/SearchInput"; +import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; -import { ThemePalette, ThemeProps } from "@src/components/Theme"; -import type { Instance as InstanceType } from "@src/@types/Instance"; - -import instanceImage from "./images/instance.svg"; +import bigInstanceImage from "./images/instance-big.svg"; import instanceLinuxImage from "./images/instance-linux.svg"; import instanceWindowsImage from "./images/instance-windows.svg"; -import bigInstanceImage from "./images/instance-big.svg"; +import instanceImage from "./images/instance.svg"; + +import type { Instance as InstanceType } from "@src/@types/Instance"; const mbToGbString = (mb: number) => mb >= 1024 ? `${(mb / 1024).toFixed(2)} GB` : `${mb} MB`; diff --git a/src/components/modules/WizardModule/WizardInstances/test.tsx b/src/components/modules/WizardModule/WizardInstances/test.tsx deleted file mode 100644 index d87d3429..00000000 --- a/src/components/modules/WizardModule/WizardInstances/test.tsx +++ /dev/null @@ -1,154 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import WizardInstances from "."; - -const wrap = props => - new TW( - shallow(), - "wInstances" - ); - -const instances = [ - { - id: "i-1", - flavor_name: "Flavor name", - instance_name: "Instance name 1", - num_cpu: 3, - memory_mb: 1024, - }, - { - id: "i-2", - flavor_name: "Flavor name", - instance_name: "Instance name 2", - num_cpu: 3, - memory_mb: 1024, - }, - { - id: "i-3", - flavor_name: "Flavor name", - instance_name: "Instance name 3", - num_cpu: 3, - memory_mb: 1024, - }, -]; -describe("WizardInstances Component", () => { - it("has correct number of instances", () => { - const wrapper = wrap({ instances, currentPage: 1 }); - expect(wrapper.findPartialId("item-").length).toBe(instances.length); - }); - - it("has correct instances info", () => { - const wrapper = wrap({ instances, currentPage: 1 }); - instances.forEach(instance => { - expect(wrapper.find(`item-${instance.id}`).findText("itemName")).toBe( - instance.instance_name - ); - expect(wrapper.find(`item-${instance.id}`).findText("itemDetails")).toBe( - `${instance.num_cpu} vCPU | ${instance.memory_mb} MB RAM | ${instance.flavor_name}` - ); - }); - }); - - it("renders selected instances", () => { - const wrapper = wrap({ - instances, - currentPage: 1, - selectedInstances: [{ ...instances[0] }, { ...instances[2] }], - instancesPerPage: 3, - }); - expect(wrapper.findText("selInfo")).toBe("2 instances selected"); - expect(wrapper.find("item-i-1").prop("selected")).toBe(true); - expect(wrapper.find("item-i-2").prop("selected")).toBe(false); - expect(wrapper.find("item-i-3").prop("selected")).toBe(true); - }); - - it("renders current page", () => { - const wrapper = wrap({ instances, currentPage: 2, instancesPerPage: 2 }); - expect(wrapper.findText("currentPage")).toBe("2 of 2"); - }); - - it("renders previous page disabled if page is 1", () => { - const wrapper = wrap({ instances, currentPage: 1 }); - expect(wrapper.find("prevPageButton").prop("disabled")).toBe(true); - }); - - it("renders previous page enabled if page is greater than 1", () => { - const wrapper = wrap({ instances, currentPage: 3 }); - expect(wrapper.find("prevPageButton").prop("disabled")).toBeFalsy(); - expect(wrapper.find("loadingStatus").length).toBe(0); - }); - - it("renders loading", () => { - const wrapper = wrap({ instances, currentPage: 1, loading: true }); - expect(wrapper.find("loadingStatus").length).toBe(1); - }); - - it("renders searching", () => { - const wrapper = wrap({ instances, currentPage: 1, searching: true }); - expect(wrapper.find("searchInput").prop("loading")).toBe(true); - }); - - it("renders search not found", () => { - const wrapper = wrap({ - instances: [], - currentPage: 1, - searchNotFound: true, - }); - expect(wrapper.findText("notFoundText")).toBe( - "Your search returned no results" - ); - expect(wrapper.find("loadingChunks").length).toBe(0); - }); - - it("renders loading page", () => { - const wrapper = wrap({ instances, currentPage: 1, chunksLoading: true }); - expect(wrapper.find("loadingChunks").length).toBe(1); - }); - - it("enabled next page", () => { - let wrapper = wrap({ instances, currentPage: 1 }); - expect(wrapper.find("nextPageButton").prop("disabled")).toBe(true); - wrapper = wrap({ instances, currentPage: 1, instancesPerPage: 2 }); - expect(wrapper.find("nextPageButton").prop("disabled")).toBeFalsy(); - }); - - it("dispatches next and previous page click, if enabled", () => { - const onPageClick = sinon.spy(); - let wrapper = wrap({ instances, currentPage: 1, onPageClick }); - wrapper.find("nextPageButton").click(); - wrapper.find("prevPageButton").click(); - expect(onPageClick.callCount).toBe(0); - wrapper = wrap({ - instances, - currentPage: 2, - onPageClick, - instancesPerPage: 1, - }); - wrapper.find("nextPageButton").click(); - wrapper.find("prevPageButton").click(); - expect(onPageClick.callCount).toBe(2); - }); - - it("dispaches reload click", () => { - const onReloadClick = sinon.spy(); - const wrapper = wrap({ instances, currentPage: 1, onReloadClick }); - wrapper.find("reloadButton").click(); - expect(onReloadClick.calledOnce).toBe(true); - }); -}); diff --git a/src/components/modules/WizardModule/WizardNetworks/WizardNetworks.spec.tsx b/src/components/modules/WizardModule/WizardNetworks/WizardNetworks.spec.tsx new file mode 100644 index 00000000..b8a3ae9c --- /dev/null +++ b/src/components/modules/WizardModule/WizardNetworks/WizardNetworks.spec.tsx @@ -0,0 +1,40 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; +import { NETWORK_MOCK } from "@tests/mocks/NetworksMock"; + +import WizardNetworks from "./"; + +describe("WizardNetworks", () => { + let defaultProps: WizardNetworks["props"]; + + beforeEach(() => { + defaultProps = { + loading: false, + loadingInstancesDetails: false, + networks: [NETWORK_MOCK], + instancesDetails: [INSTANCE_MOCK], + onChange: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(NETWORK_MOCK.name)).toBeTruthy(); + }); +}); diff --git a/src/components/modules/WizardModule/WizardNetworks/WizardNetworks.tsx b/src/components/modules/WizardModule/WizardNetworks/WizardNetworks.tsx index 356c3fd7..423e0a5d 100644 --- a/src/components/modules/WizardModule/WizardNetworks/WizardNetworks.tsx +++ b/src/components/modules/WizardModule/WizardNetworks/WizardNetworks.tsx @@ -12,21 +12,21 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled from "styled-components"; +import { Instance, InstanceUtils, Nic as NicType } from "@src/@types/Instance"; +import { ThemePalette, ThemeProps } from "@src/components/Theme"; import AutocompleteDropdown from "@src/components/ui/Dropdowns/AutocompleteDropdown"; -import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; import Dropdown from "@src/components/ui/Dropdowns/Dropdown"; +import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; -import { ThemePalette, ThemeProps } from "@src/components/Theme"; -import { Instance, InstanceUtils, Nic as NicType } from "@src/@types/Instance"; -import type { Network, NetworkMap, SecurityGroup } from "@src/@types/Network"; - -import networkImage from "./images/network.svg"; -import bigNetworkImage from "./images/network-big.svg"; import arrowImage from "./images/arrow.svg"; +import bigNetworkImage from "./images/network-big.svg"; +import networkImage from "./images/network.svg"; + +import type { Network, NetworkMap, SecurityGroup } from "@src/@types/Network"; const Wrapper = styled.div` width: 100%; diff --git a/src/components/modules/WizardModule/WizardNetworks/test.tsx b/src/components/modules/WizardModule/WizardNetworks/test.tsx deleted file mode 100644 index 4d54cbcd..00000000 --- a/src/components/modules/WizardModule/WizardNetworks/test.tsx +++ /dev/null @@ -1,114 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import WizardNetworks from "."; - -const wrap = props => - new TW(shallow(), "wNetworks"); - -const networks = [ - { name: "network 1", value: "n-1" }, - { name: "network 2", value: "n-2" }, -]; - -const instancesDetails = [ - { - devices: { nics: [{ network_name: "network 1", id: "n-1" }] }, - instance_name: "Instance name 1", - }, - { - devices: { nics: [{ network_name: "network 2", id: "n-2" }] }, - instance_name: "Instance name 2", - }, - { - devices: { nics: [{ network_name: "network 3", id: "n-3" }] }, - instance_name: "Instance name 3", - }, -]; - -const selectedNetworks = [ - { - sourceNic: { id: "n-2", network_name: "network 2" }, - targetNetwork: { name: "network 1" }, - }, -]; - -describe("WizardNetworks Component", () => { - it("renders correct number of instance details", () => { - const wrapper = wrap({ networks, instancesDetails }); - expect(wrapper.findPartialId("dropdown-").length).toBe( - instancesDetails.length - ); - }); - - it("renders correct info for instance details", () => { - const wrapper = wrap({ networks, instancesDetails }); - expect(wrapper.findText("connectedTo-n-1")).toBe( - "Connected to Instance name 1" - ); - expect(wrapper.findText("connectedTo-n-2")).toBe( - "Connected to Instance name 2" - ); - expect(wrapper.findText("connectedTo-n-3")).toBe( - "Connected to Instance name 3" - ); - expect(wrapper.findText("networkName-n-1")).toBe("network 1"); - expect(wrapper.findText("networkName-n-2")).toBe("network 2"); - expect(wrapper.findText("networkName-n-3")).toBe("network 3"); - }); - - it("has dropdown with correct number of networks", () => { - const wrapper = wrap({ networks, instancesDetails }); - expect(wrapper.find("dropdown-n-1").prop("items").length).toBe( - networks.length - ); - expect(wrapper.find("dropdown-n-2").prop("items").length).toBe( - networks.length - ); - expect(wrapper.find("dropdown-n-3").prop("items").length).toBe( - networks.length - ); - }); - - it("has dropdown with correct networks info", () => { - const wrapper = wrap({ networks, instancesDetails }); - expect(wrapper.find("dropdown-n-1").prop("items")[0].name).toBe( - "network 1" - ); - expect(wrapper.find("dropdown-n-2").prop("items")[1].name).toBe( - "network 2" - ); - }); - - it("renders selected networks", () => { - const wrapper = wrap({ networks, instancesDetails, selectedNetworks }); - expect(wrapper.find("dropdown-n-1").prop("selectedItem")).toBeFalsy(); - expect(wrapper.find("dropdown-n-2").prop("selectedItem").name).toBe( - "network 1" - ); - expect(wrapper.find("dropdown-n-3").prop("selectedItem")).toBeFalsy(); - expect(wrapper.find("noNics").length).toBe(0); - }); - - it("renders no nics message", () => { - const wrapper = wrap({ - networks, - instancesDetails: [{ ...instancesDetails[0], devices: { nics: [] } }], - }); - expect(wrapper.find("noNics").length).toBe(1); - }); -}); diff --git a/src/components/modules/WizardModule/WizardOptions/WizardOptions.spec.tsx b/src/components/modules/WizardModule/WizardOptions/WizardOptions.spec.tsx new file mode 100644 index 00000000..fb547ef3 --- /dev/null +++ b/src/components/modules/WizardModule/WizardOptions/WizardOptions.spec.tsx @@ -0,0 +1,50 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { MINION_POOL_MOCK } from "@tests/mocks/MinionPoolMock"; + +import WizardOptions from "./"; + +jest.mock("@src/plugins/default/ContentPlugin", () => jest.fn(() => null)); +jest.mock("@src/utils/Config", () => ({ + config: { + passwordFields: ["secret_key"], + }, +})); +jest.mock("react-transition-group", () => ({ + CSSTransitionGroup: (props: any) =>
{props.children}
, +})); + +describe("WizardOptions", () => { + let defaultProps: WizardOptions["props"]; + + beforeEach(() => { + defaultProps = { + fields: [{ name: "field1", label: "Field 1", type: "string" }], + minionPools: [MINION_POOL_MOCK], + hasStorageMap: false, + wizardType: "replica", + dictionaryKey: "replica", + onChange: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Execute Now")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/WizardModule/WizardOptions/test.tsx b/src/components/modules/WizardModule/WizardOptions/test.tsx deleted file mode 100644 index e30906dd..00000000 --- a/src/components/modules/WizardModule/WizardOptions/test.tsx +++ /dev/null @@ -1,138 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import WizardOptions from "."; - -import configLoader from "@src/utils/Config"; - -const wrap = props => new TW(shallow(), "wOptions"); - -const fields = [ - { - name: "string_field", - type: "string", - }, - { - name: "string_field_with_default", - type: "string", - default: "default", - }, - { - required: true, - name: "required_string_field", - type: "string", - }, - { - name: "enum_field", - type: "string", - - enum: ["enum 1", "enum 2", "enum 3"], - }, - { - name: "boolean_field", - type: "boolean", - }, - { - name: "strict_boolean_field", - type: "strict-boolean", - }, -]; - -describe("WizardOptions Component", () => { - beforeAll(() => { - configLoader.config = { passwordFields: [] }; - }); - - it("has description and required field in simple tab", () => { - const wrapper = wrap({ - fields, - selectedInstances: [], - wizardType: "migration", - }); - expect(wrapper.findPartialId("field-").length).toBe(5); - expect(wrapper.find("field-description").length).toBe(1); - expect(wrapper.find("field-required_string_field").length).toBe(1); - }); - - it("renders execute now for replica", () => { - const wrapper = wrap({ - fields, - selectedInstances: [], - wizardType: "replica", - }); - expect(wrapper.find("field-execute_now").length).toBe(1); - expect(wrapper.find("field-execute_now_options").length).toBe(1); - }); - - it("renders skip os morphing for migration", () => { - const wrapper = wrap({ - fields, - selectedInstances: [], - wizardType: "migration", - }); - expect(wrapper.find("field-skip_os_morphing").length).toBe(1); - }); - - it("renders separate / vm if multiple instances are selected", () => { - const wrapper = wrap({ fields, selectedInstances: [{}, {}] }); - expect(wrapper.find("field-separate_vm").length).toBe(1); - }); - - it("renders correct number of fields in advanced tab", () => { - const wrapper = wrap({ - fields, - selectedInstances: [], - useAdvancedOptions: true, - wizardType: "migration", - }); - expect(wrapper.findPartialId("field-").length).toBe(fields.length + 4); - }); - - it("renders correct field info", () => { - const wrapper = wrap({ - fields, - selectedInstances: [], - useAdvancedOptions: true, - wizardType: "migration", - }); - - expect(wrapper.find("field-description").prop("type")).toBe("string"); - expect(wrapper.find("field-required_string_field").prop("required")).toBe( - true - ); - expect(wrapper.find("field-string_field").prop("type")).toBe("string"); - expect(wrapper.find("field-string_field_with_default").prop("value")).toBe( - "default" - ); - expect(wrapper.find("field-enum_field").prop("enum")[0]).toBe("enum 1"); - expect(wrapper.find("field-enum_field").prop("enum")[1]).toBe("enum 2"); - expect(wrapper.find("field-boolean_field").prop("type")).toBe("boolean"); - expect(wrapper.find("field-strict_boolean_field").prop("type")).toBe( - "strict-boolean" - ); - }); - - it("renders data into field", () => { - const wrapper = wrap({ - fields, - selectedInstances: [], - useAdvancedOptions: true, - data: { string_field: "new data" }, - }); - expect(wrapper.find("field-string_field").prop("value")).toBe("new data"); - }); -}); diff --git a/src/components/modules/WizardModule/WizardPageContent/WizardPageContent.tsx b/src/components/modules/WizardModule/WizardPageContent/WizardPageContent.tsx index 65a7bd89..7601ef48 100644 --- a/src/components/modules/WizardModule/WizardPageContent/WizardPageContent.tsx +++ b/src/components/modules/WizardModule/WizardPageContent/WizardPageContent.tsx @@ -12,46 +12,44 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +import { observer } from "mobx-react"; import React from "react"; import styled from "styled-components"; -import { observer } from "mobx-react"; +import { Endpoint, EndpointUtils, StorageMap } from "@src/@types/Endpoint"; +import { ProviderTypes } from "@src/@types/Providers"; import EndpointLogos from "@src/components/modules/EndpointModule/EndpointLogos"; -import WizardType from "@src/components/modules/WizardModule/WizardType"; -import Button from "@src/components/ui/Button"; -import InfoIcon from "@src/components/ui/InfoIcon"; +import Schedule from "@src/components/modules/TransferModule/Schedule"; import WizardBreadcrumbs from "@src/components/modules/WizardModule/WizardBreadcrumbs"; import WizardEndpointList from "@src/components/modules/WizardModule/WizardEndpointList"; import WizardInstances from "@src/components/modules/WizardModule/WizardInstances"; import WizardNetworks, { WizardNetworksChangeObject, } from "@src/components/modules/WizardModule/WizardNetworks"; -import WizardStorage from "@src/components/modules/WizardModule/WizardStorage"; import WizardOptions from "@src/components/modules/WizardModule/WizardOptions"; import WizardScripts from "@src/components/modules/WizardModule/WizardScripts"; -import Schedule from "@src/components/modules/TransferModule/Schedule"; +import WizardStorage from "@src/components/modules/WizardModule/WizardStorage"; import WizardSummary from "@src/components/modules/WizardModule/WizardSummary"; - +import WizardType from "@src/components/modules/WizardModule/WizardType"; import { ThemePalette, ThemeProps } from "@src/components/Theme"; -import { providerTypes, wizardPages, migrationFields } from "@src/constants"; +import Button from "@src/components/ui/Button"; +import InfoIcon from "@src/components/ui/InfoIcon"; +import LoadingButton from "@src/components/ui/LoadingButton"; +import { migrationFields, providerTypes, wizardPages } from "@src/constants"; +import endpointStore from "@src/stores/EndpointStore"; +import instanceStore from "@src/stores/InstanceStore"; +import minionPoolStore from "@src/stores/MinionPoolStore"; +import networkStore from "@src/stores/NetworkStore"; +import notificationStore from "@src/stores/NotificationStore"; +import providerStore from "@src/stores/ProviderStore"; import configLoader from "@src/utils/Config"; +import transferItemIcon from "./images/transferItemIcon"; + import type { WizardData, WizardPage } from "@src/@types/WizardData"; -import { Endpoint, EndpointUtils, StorageMap } from "@src/@types/Endpoint"; import type { Instance, InstanceScript } from "@src/@types/Instance"; import type { Field } from "@src/@types/Field"; import type { Schedule as ScheduleType } from "@src/@types/Schedule"; -import instanceStore from "@src/stores/InstanceStore"; -import providerStore from "@src/stores/ProviderStore"; -import endpointStore from "@src/stores/EndpointStore"; -import networkStore from "@src/stores/NetworkStore"; - -import { ProviderTypes } from "@src/@types/Providers"; -import minionPoolStore from "@src/stores/MinionPoolStore"; -import LoadingButton from "@src/components/ui/LoadingButton"; -import notificationStore from "@src/stores/NotificationStore"; -import transferItemIcon from "./images/transferItemIcon"; - const Wrapper = styled.div` ${ThemeProps.exactWidth(`${parseInt(ThemeProps.contentWidth, 10) + 64}px`)} margin: 64px auto 32px auto; diff --git a/src/components/modules/WizardModule/WizardScripts/WizardScripts.spec.tsx b/src/components/modules/WizardModule/WizardScripts/WizardScripts.spec.tsx new file mode 100644 index 00000000..6f5cc0df --- /dev/null +++ b/src/components/modules/WizardModule/WizardScripts/WizardScripts.spec.tsx @@ -0,0 +1,41 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; + +import WizardScripts from "./"; + +describe("WizardScripts", () => { + let defaultProps: WizardScripts["props"]; + + beforeEach(() => { + defaultProps = { + instances: [INSTANCE_MOCK], + uploadedScripts: [], + removedScripts: [], + userScriptData: null, + onScriptUpload: jest.fn(), + onCancelScript: jest.fn(), + onScriptDataRemove: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText(INSTANCE_MOCK.name)).toBeTruthy(); + }); +}); diff --git a/src/components/modules/WizardModule/WizardScripts/WizardScripts.tsx b/src/components/modules/WizardModule/WizardScripts/WizardScripts.tsx index db03e283..3900a883 100644 --- a/src/components/modules/WizardModule/WizardScripts/WizardScripts.tsx +++ b/src/components/modules/WizardModule/WizardScripts/WizardScripts.tsx @@ -12,23 +12,22 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled, { css } from "styled-components"; -import InfoIcon from "@src/components/ui/InfoIcon"; -import { Close as InputClose } from "@src/components/ui/TextInput"; +import { UserScriptData } from "@src/@types/MainItem"; import { InstanceImage } from "@src/components/modules/WizardModule/WizardInstances"; -import StatusIcon from "@src/components/ui/StatusComponents/StatusIcon"; - import { ThemePalette, ThemeProps } from "@src/components/Theme"; +import InfoIcon from "@src/components/ui/InfoIcon"; +import StatusIcon from "@src/components/ui/StatusComponents/StatusIcon"; +import { Close as InputClose } from "@src/components/ui/TextInput"; +import DomUtils from "@src/utils/DomUtils"; import FileUtils from "@src/utils/FileUtils"; -import type { Instance, InstanceScript } from "@src/@types/Instance"; -import { UserScriptData } from "@src/@types/MainItem"; -import DomUtils from "@src/utils/DomUtils"; import scriptItemImage from "./images/script-item.svg"; +import type { Instance, InstanceScript } from "@src/@types/Instance"; const Wrapper = styled.div` width: 100%; display: flex; diff --git a/src/components/modules/WizardModule/WizardStorage/WizardStorage.spec.tsx b/src/components/modules/WizardModule/WizardStorage/WizardStorage.spec.tsx new file mode 100644 index 00000000..b084543d --- /dev/null +++ b/src/components/modules/WizardModule/WizardStorage/WizardStorage.spec.tsx @@ -0,0 +1,43 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; +import { STORAGE_BACKEND_MOCK } from "@tests/mocks/StoragesMock"; + +import WizardStorage from "./"; + +describe("WizardStorage", () => { + let defaultProps: WizardStorage["props"]; + + beforeEach(() => { + defaultProps = { + storageBackends: [STORAGE_BACKEND_MOCK], + loading: false, + instancesDetails: [INSTANCE_MOCK], + storageMap: null, + defaultStorageLayout: "page", + defaultStorage: { value: STORAGE_BACKEND_MOCK.id }, + onDefaultStorageChange: jest.fn(), + onChange: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Storage Backend Mapping")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/WizardModule/WizardStorage/WizardStorage.tsx b/src/components/modules/WizardModule/WizardStorage/WizardStorage.tsx index f1179ace..da5c169a 100644 --- a/src/components/modules/WizardModule/WizardStorage/WizardStorage.tsx +++ b/src/components/modules/WizardModule/WizardStorage/WizardStorage.tsx @@ -12,24 +12,24 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled from "styled-components"; +import { Disk, Instance, InstanceUtils } from "@src/@types/Instance"; +import { ThemePalette, ThemeProps } from "@src/components/Theme"; +import Button from "@src/components/ui/Button"; import AutocompleteDropdown from "@src/components/ui/Dropdowns/AutocompleteDropdown"; import Dropdown from "@src/components/ui/Dropdowns/Dropdown"; import InfoIcon from "@src/components/ui/InfoIcon"; - -import { ThemePalette, ThemeProps } from "@src/components/Theme"; -import { Instance, Disk, InstanceUtils } from "@src/@types/Instance"; -import type { StorageBackend, StorageMap } from "@src/@types/Endpoint"; - import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; -import Button from "@src/components/ui/Button"; + +import arrowImage from "./images/arrow.svg"; import backendImage from "./images/backend.svg"; import diskImage from "./images/disk.svg"; import bigStorageImage from "./images/storage-big.svg"; -import arrowImage from "./images/arrow.svg"; + +import type { StorageBackend, StorageMap } from "@src/@types/Endpoint"; const Wrapper = styled.div` width: 100%; @@ -178,8 +178,6 @@ export const getDisks = ( return disks; }; -export const TEST_ID = "wizardStorage"; - export type Props = { storageBackends: StorageBackend[]; loading: boolean; diff --git a/src/components/modules/WizardModule/WizardStorage/test.tsx b/src/components/modules/WizardModule/WizardStorage/test.tsx deleted file mode 100644 index 32812d79..00000000 --- a/src/components/modules/WizardModule/WizardStorage/test.tsx +++ /dev/null @@ -1,150 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; - -import type { Instance } from "@src/@types/Instance"; -import TW from "@src/utils/TestWrapper"; -import Component, { TEST_ID } from "."; -import type { Props } from "."; - -const defaultInstance: Instance = { - id: "instance1id", - name: "instance1name", - flavor_name: "instance1flavorname", - instance_name: "instance1instancename", - num_cpu: 2, - memory_mb: 2048, - os_type: "windows", - devices: { - nics: [ - { - id: "instance1nic1", - network_name: "network1", - mac_address: "instance1macaddress", - network_id: "network1", - }, - ], - disks: [ - { - id: "disk1-id", - name: "disk1-name", - storage_backend_identifier: "sback2", - }, - ], - }, -}; - -const defaultProps: Props = { - storageBackends: [ - { - id: "sback1", - name: "sback1-name", - }, - { - id: "sback2", - name: "sback2-name", - }, - ], - instancesDetails: [defaultInstance], - storageMap: [ - { - type: "disk", - source: { id: "disk1-id" }, - target: { id: "sback2", name: "sback2-name" }, - }, - { - type: "backend", - source: { id: "sback2", storage_backend_identifier: "sback2" }, - target: { id: "sback1", name: "sback1-name" }, - }, - ], - defaultStorage: "sback1", - onChange: () => {}, - defaultStorageLayout: "page", - onDefaultStorageChange: () => {}, - storageConfigDefault: null, -}; -const wrap = (props: Props) => - new TW(shallow(), TEST_ID); - -describe("WizardStorage Component", () => { - it("renders backend mapping", () => { - const wrapper = wrap(defaultProps); - - expect(wrapper.findText("backend-source")).toBe("sback2"); - expect(wrapper.findText("backend-connectedTo")).toBe( - "Connected to instance1instancename" - ); - - expect(wrapper.find("backend-destination").prop("selectedItem").id).toBe( - "sback1" - ); - - expect(wrapper.find("backend-destination").prop("items")[0].id).toBe(null); - expect(wrapper.find("backend-destination").prop("items")[0].name).toBe( - "Default" - ); - expect(wrapper.find("backend-destination").prop("items")[1].id).toBe( - "sback1" - ); - expect(wrapper.find("backend-destination").prop("items")[2].id).toBe( - "sback2" - ); - }); - - it("renders disk mapping", () => { - const wrapper = wrap(defaultProps); - expect(wrapper.findText("disk-source")).toBe("disk1-id"); - expect(wrapper.findText("disk-connectedTo")).toBe( - "Connected to instance1instancename" - ); - - expect(wrapper.find("disk-destination").prop("selectedItem").id).toBe( - "sback2" - ); - - expect(wrapper.find("disk-destination").prop("items")[0].id).toBe(null); - expect(wrapper.find("disk-destination").prop("items")[0].name).toBe( - "Default" - ); - expect(wrapper.find("disk-destination").prop("items")[1].id).toBe("sback1"); - expect(wrapper.find("disk-destination").prop("items")[2].id).toBe("sback2"); - }); - - it("renders no storage message", () => { - const newProps: Props = { ...defaultProps }; - newProps.storageBackends = []; - let wrapper = wrap(newProps); - expect(wrapper.find("noStorage").length).toBe(1); - wrapper = wrap(defaultProps); - expect(wrapper.find("noStorage").length).toBe(0); - }); - - it("dispatches change", () => { - const newProps: Props = { ...defaultProps, onChange: sinon.spy() }; - const wrapper = wrap(newProps); - wrapper - .find("disk-destination") - .simulate("change", { id: "sback2", name: "sback2-name" }); - - const arg = newProps.onChange.args[0]; - - expect(arg[0].id).toBe("disk1-id"); - expect(arg[1].id).toBe("sback2"); - expect(arg[2]).toBe("disk"); - }); -}); diff --git a/src/components/modules/WizardModule/WizardSummary/WizardSummary.spec.tsx b/src/components/modules/WizardModule/WizardSummary/WizardSummary.spec.tsx new file mode 100644 index 00000000..cd6ea071 --- /dev/null +++ b/src/components/modules/WizardModule/WizardSummary/WizardSummary.spec.tsx @@ -0,0 +1,71 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; +import { + OPENSTACK_ENDPOINT_MOCK, + VMWARE_ENDPOINT_MOCK, +} from "@tests/mocks/EndpointsMock"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; +import { MINION_POOL_MOCK } from "@tests/mocks/MinionPoolMock"; +import { SCHEDULE_MOCK } from "@tests/mocks/SchedulesMock"; + +import WizardSummary from "./"; + +jest.mock("@src/utils/Config", () => ({ + config: { + providerSortPriority: {}, + providerNames: { + openstack: "OpenStack", + vmware_vsphere: "VMware vSphere", + }, + }, +})); + +describe("WizardSummary", () => { + let defaultProps: WizardSummary["props"]; + + beforeEach(() => { + defaultProps = { + data: { + destOptions: { option_1: "option_1" }, + sourceOptions: { option_2: "option_2" }, + selectedInstances: [INSTANCE_MOCK], + source: VMWARE_ENDPOINT_MOCK, + target: OPENSTACK_ENDPOINT_MOCK, + }, + wizardType: "replica", + schedules: [SCHEDULE_MOCK], + minionPools: [MINION_POOL_MOCK], + defaultStorage: { value: "defaultStorage" }, + instancesDetails: [INSTANCE_MOCK], + storageMap: [], + sourceSchema: [{ name: "option_1", label: "Option 1", type: "string" }], + destinationSchema: [ + { name: "option_2", label: "Option 2", type: "string" }, + ], + uploadedUserScripts: [], + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Overview")).toBeTruthy(); + expect(getByText("REPLICA")).toBeTruthy(); + expect(getByText(VMWARE_ENDPOINT_MOCK.name)).toBeTruthy(); + expect(getByText(INSTANCE_MOCK.name)).toBeTruthy(); + }); +}); diff --git a/src/components/modules/WizardModule/WizardSummary/test.tsx b/src/components/modules/WizardModule/WizardSummary/test.tsx deleted file mode 100644 index f2c4c2ec..00000000 --- a/src/components/modules/WizardModule/WizardSummary/test.tsx +++ /dev/null @@ -1,112 +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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import WizardSummary from "."; - -const wrap = props => - new TW( - shallow( - - ), - "wSummary" - ); - -const schedules = [ - { - id: "s-1", - schedule: { - month: 2, - dom: 14, - dow: 3, - minute: 0, - hour: 17, - }, - }, -]; - -const data = { - destOptions: { - description: "A description", - field_name: "Field name value", - }, - selectedInstances: [ - { - flavor_name: "flavor_name", - id: "i-1", - name: "name", - num_cpu: 2, - memory_mb: 1024, - }, - ], - networks: [ - { - sourceNic: { id: "s-1", network_name: "n-1" }, - targetNetwork: { name: "target network" }, - }, - ], - source: { - type: "openstack", - name: "source name", - }, - target: { - type: "azure", - name: "target name", - }, -}; - -describe("WizardSummary Component", () => { - it("renders overview section", () => { - const wrapper = wrap({ data, wizardType: "replica" }); - expect(wrapper.findText("source")).toBe("source name"); - expect(wrapper.find("sourcePill").prop("label")).toBe("OPENSTACK"); - expect(wrapper.findText("target")).toBe("target name"); - expect(wrapper.find("targetPill").prop("label")).toBe("AZURE"); - expect(wrapper.find("typePill").prop("label")).toBe("REPLICA"); - }); - - it("renders instances section", () => { - const wrapper = wrap({ data, wizardType: "replica" }); - expect(wrapper.findText("instance-i-1")).toBe("name"); - }); - - it("renders networks section", () => { - const wrapper = wrap({ data, wizardType: "replica" }); - expect(wrapper.findText("networkSource")).toBe("n-1"); - expect(wrapper.findText("networkTarget")).toBe("target network"); - }); - - it("renders options section", () => { - const wrapper = wrap({ data, wizardType: "replica" }); - expect(wrapper.findText("optionLabel-description")).toBe("Description"); - expect(wrapper.findText("optionValue-description")).toBe("A description"); - expect(wrapper.findText("optionLabel-field_name")).toBe("Field Name"); - expect(wrapper.findText("optionValue-field_name")).toBe("Field name value"); - }); - - it("renders schedule section", () => { - const wrapper = wrap({ data, schedules, wizardType: "replica" }); - expect(wrapper.findText(`scheduleItem-${schedules[0].id}`)).toBe( - "Every February, every 14th, every Wednesday, at 17:00 UTC" - ); - }); -}); diff --git a/src/components/modules/WizardModule/WizardType/WizardType.spec.tsx b/src/components/modules/WizardModule/WizardType/WizardType.spec.tsx new file mode 100644 index 00000000..84a3c4b2 --- /dev/null +++ b/src/components/modules/WizardModule/WizardType/WizardType.spec.tsx @@ -0,0 +1,35 @@ +/* +Copyright (C) 2023 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 . +*/ + +import React from "react"; + +import { render } from "@testing-library/react"; + +import WizardType from "./"; + +describe("WizardType", () => { + let defaultProps: WizardType["props"]; + + beforeEach(() => { + defaultProps = { + selected: "replica", + onChange: jest.fn(), + }; + }); + + it("renders without crashing", () => { + const { getByText } = render(); + expect(getByText("Coriolis Replica")).toBeTruthy(); + }); +}); diff --git a/src/components/modules/WizardModule/WizardType/WizardType.tsx b/src/components/modules/WizardModule/WizardType/WizardType.tsx index c7369929..fadd36a1 100644 --- a/src/components/modules/WizardModule/WizardType/WizardType.tsx +++ b/src/components/modules/WizardModule/WizardType/WizardType.tsx @@ -12,13 +12,12 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled from "styled-components"; -import Switch from "@src/components/ui/Switch"; - import { ThemePalette, ThemeProps } from "@src/components/Theme"; +import Switch from "@src/components/ui/Switch"; import migrationImage from "./images/migration"; diff --git a/src/components/modules/WizardModule/WizardType/test.tsx b/src/components/modules/WizardModule/WizardType/test.tsx deleted file mode 100644 index a607ccdc..00000000 --- a/src/components/modules/WizardModule/WizardType/test.tsx +++ /dev/null @@ -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 . -*/ - -import React from "react"; -import { shallow } from "enzyme"; -import sinon from "sinon"; -import TW from "@src/utils/TestWrapper"; -import WizardType from "."; - -const wrap = props => - new TW(shallow( {}} {...props} />), "wType"); - -describe("WizardType Component", () => { - it("renders with the correct type selected", () => { - let wrapper = wrap({ selected: "migration" }); - expect(wrapper.find("switch").prop("checked")).toBe(false); - wrapper = wrap({ selected: "replica" }); - expect(wrapper.find("switch").prop("checked")).toBe(true); - }); - - it("dispatches change", () => { - const onChange = sinon.spy(); - const wrapper = wrap({ selected: "replica", onChange }); - wrapper.find("switch").simulate("change", { passed: true }); - expect(onChange.args[0][0].passed).toBe(true); - }); -}); diff --git a/src/components/ui/AlertModal/AlertModal.spec.tsx b/src/components/ui/AlertModal/AlertModal.spec.tsx index 2763a5f8..37030252 100644 --- a/src/components/ui/AlertModal/AlertModal.spec.tsx +++ b/src/components/ui/AlertModal/AlertModal.spec.tsx @@ -1,5 +1,5 @@ /* -Copyright (C) 2017 Cloudbase Solutions SRL +Copyright (C) 2023 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,59 +13,175 @@ along with this program. If not, see . */ import React from "react"; -import { render } from "@testing-library/react"; -import StatusImage from "@src/components/ui/StatusComponents/StatusImage"; -import TestUtils from "@tests/TestUtils"; +import { render, fireEvent } from "@testing-library/react"; import AlertModal from "./AlertModal"; +import KeyboardManager from "@src/utils/KeyboardManager"; -jest.mock("../StatusComponents/StatusImage/StatusImage", () => - jest.fn(() => null) -); +jest.mock("@src/utils/KeyboardManager", () => ({ + onEnter: jest.fn(), + removeKeyDown: jest.fn(), +})); describe("AlertModal", () => { - it("renders confirmation as default with message and extra message", () => { - const message = "message"; - const extraMessage = "extra message"; - const { queryByText } = render( - + it("renders with default props", () => { + const { getByText } = render(); + expect(getByText("Yes")).toBeTruthy(); + expect(getByText("No")).toBeTruthy(); + }); + + it("adds and removes a keyboard listener on mount and unmount", () => { + const { unmount } = render(); + + expect(KeyboardManager.onEnter).toHaveBeenCalled(); + unmount(); + expect(KeyboardManager.removeKeyDown).toHaveBeenCalled(); + }); + + it("calls onConfirmation when Enter is pressed", () => { + const onConfirmationMock = jest.fn(); + render(); + + // @ts-ignore + const mockCallback = KeyboardManager.onEnter.mock.calls[0][1]; + mockCallback(); + + expect(onConfirmationMock).toHaveBeenCalled(); + }); + + it("renders error type correctly", () => { + const { queryByText, getByText } = render( + + ); + + expect(getByText("Dismiss")).toBeTruthy(); + expect(queryByText("Yes")).not.toBeTruthy(); + expect(queryByText("No")).not.toBeTruthy(); + }); + + it("renders confirmation type correctly", () => { + const { getByText } = render(); + + expect(getByText("Yes")).toBeTruthy(); + expect(getByText("No")).toBeTruthy(); + }); + + it("renders loading type correctly", () => { + const { queryByText } = render(); + + expect(queryByText("Yes")).not.toBeTruthy(); + expect(queryByText("No")).not.toBeTruthy(); + expect(queryByText("Dismiss")).not.toBeTruthy(); + }); + + it("calls onRequestClose when No or Dismiss is clicked", () => { + const onRequestCloseMock = jest.fn(); + + const { getByText } = render( + + ); + + fireEvent.click(getByText("Dismiss")); + expect(onRequestCloseMock).toHaveBeenCalled(); + + const { getByText: getByTextConfirmation } = render( + + ); + + fireEvent.click(getByTextConfirmation("No")); + expect(onRequestCloseMock).toHaveBeenCalledTimes(2); + }); + + it("calls onConfirmation when Yes is clicked", () => { + const onConfirmationMock = jest.fn(); + + const { getByText } = render( + ); - expect(TestUtils.select("AlertModal__Message")?.innerHTML).toBe(message); - expect(TestUtils.select("AlertModal__ExtraMessage")?.textContent).toBe( - extraMessage + + fireEvent.click(getByText("Yes")); + expect(onConfirmationMock).toHaveBeenCalled(); + }); + + it("renders the message when provided", () => { + const customMessage = "This is a custom message"; + const { getByText } = render(); + expect(getByText(customMessage)).toBeTruthy(); + }); + + it("does not render the message when not provided", () => { + const { queryByText } = render(); + expect(queryByText(/This is a custom message/i)).not.toBeTruthy(); + }); + + it("renders the extraMessage when provided", () => { + const customExtraMessage = "This is an extra message"; + const { getByText } = render( + ); + expect(getByText(customExtraMessage)).toBeTruthy(); + }); + + it("does not render the extraMessage when not provided", () => { + const { queryByText } = render(); + expect(queryByText(/This is an extra message/i)).not.toBeTruthy(); + }); + + it("renders confirmation buttons when type is confirmation", () => { + const { getByText } = render(); - expect(queryByText("No")).toBeTruthy(); - expect(queryByText("Yes")).toBeTruthy(); - expect(queryByText("Dismiss")).toBeNull(); - expect(StatusImage).toHaveBeenCalledWith({ status: "confirmation" }, {}); + expect(getByText("Yes")).toBeTruthy(); + expect(getByText("No")).toBeTruthy(); }); - it("has correct buttons for errors", () => { - const { queryByText } = render( + it("renders message and extraMessage for confirmation", () => { + const customMessage = "Confirm this action?"; + const customExtraMessage = "This will perform a special task."; + const { getByText } = render( ); - expect(queryByText("Dismiss")).toBeTruthy(); - expect(queryByText("No")).toBeNull(); - expect(queryByText("Yes")).toBeNull(); + + expect(getByText(customMessage)).toBeTruthy(); + expect(getByText(customExtraMessage)).toBeTruthy(); }); - it("renders loading", () => { - const { queryByText } = render( + it("calls onConfirmation when Yes is clicked", () => { + const onConfirmationMock = jest.fn(); + const { getByText } = render( ); - expect(queryByText("Dismiss")).toBeNull(); - expect(queryByText("No")).toBeNull(); - expect(queryByText("Yes")).toBeNull(); - expect(StatusImage).toHaveBeenCalledWith({ status: "RUNNING" }, {}); + + fireEvent.click(getByText("Yes")); + expect(onConfirmationMock).toHaveBeenCalled(); + }); + + it("calls onRequestClose when No is clicked", () => { + const onRequestCloseMock = jest.fn(); + const { getByText } = render( + + ); + + fireEvent.click(getByText("No")); + expect(onRequestCloseMock).toHaveBeenCalled(); }); }); diff --git a/src/components/ui/AutocompleteInput/test.tsx b/src/components/ui/AutocompleteInput/test.tsx deleted file mode 100644 index 4e504f35..00000000 --- a/src/components/ui/AutocompleteInput/test.tsx +++ /dev/null @@ -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 . -*/ - -import React from "react"; -import sinon from "sinon"; -import { shallow } from "enzyme"; -import TW from "@src/utils/TestWrapper"; -import AutocompleteInput from "."; - -type Props = { - value: string; - customRef?: (ref: HTMLElement) => void; - ref?: (ref: HTMLElement) => void; - onChange: (value: string) => void; - onClick?: () => void; - disabled?: boolean; - width?: number; - large?: boolean; - onFocus?: () => void; - highlight?: boolean; -}; - -const wrap = (props: Props) => - new TW( - shallow( - // eslint-disable-next-line react/jsx-props-no-spreading - - ), - "acInput" - ); - -describe("AutocompleteInput Component", () => { - it("renders input with correct data", () => { - const wrapper = wrap({ - value: "value", - onChange: () => {}, - }); - - expect(wrapper.find("text").prop("embedded")).toBe(true); - expect(wrapper.find("text").prop("value")).toBe("value"); - }); - - it("dispatches click", () => { - const onClick = sinon.spy(); - const wrapper = wrap({ - value: "value", - onChange: () => {}, - onClick, - }); - wrapper.find("arrow").click(); - expect(onClick.calledOnce).toBe(true); - }); -}); diff --git a/src/components/ui/Dropdowns/ActionDropdown/ActionDropdown.tsx b/src/components/ui/Dropdowns/ActionDropdown/ActionDropdown.tsx index 159f0d1d..d640df09 100644 --- a/src/components/ui/Dropdowns/ActionDropdown/ActionDropdown.tsx +++ b/src/components/ui/Dropdowns/ActionDropdown/ActionDropdown.tsx @@ -12,20 +12,19 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +import autobind from "autobind-decorator"; +import { observer } from "mobx-react"; import React from "react"; import ReactDOM from "react-dom"; -import { observer } from "mobx-react"; import styled, { css } from "styled-components"; -import autobind from "autobind-decorator"; +import { ThemePalette, ThemeProps } from "@src/components/Theme"; import DropdownButton from "@src/components/ui/Dropdowns/DropdownButton"; import { List, ListItems, Tip, } from "@src/components/ui/Dropdowns/DropdownLink"; - -import { ThemePalette, ThemeProps } from "@src/components/Theme"; import StatusIcon from "@src/components/ui/StatusComponents/StatusIcon"; const Wrapper = styled.div` @@ -64,7 +63,7 @@ const ListStyle = css` ${ThemeProps.boxShadow} border: none; `; -export const TEST_ID = "actionDropdown"; + export type DropdownAction = { label: string; color?: string; diff --git a/src/components/ui/Logo/Logo.spec.tsx b/src/components/ui/Logo/Logo.spec.tsx index 09a067b3..a4edc1d8 100644 --- a/src/components/ui/Logo/Logo.spec.tsx +++ b/src/components/ui/Logo/Logo.spec.tsx @@ -13,8 +13,9 @@ along with this program. If not, see . */ import React from "react"; -import { render } from "@testing-library/react"; + import Logo from "@src/components/ui/Logo"; +import { render } from "@testing-library/react"; import TestUtils from "@tests/TestUtils"; jest.mock("react-router-dom", () => ({ Link: "a" })); diff --git a/src/components/ui/SmallLoading/SmallLoading.tsx b/src/components/ui/SmallLoading/SmallLoading.tsx index a4815276..444a6185 100644 --- a/src/components/ui/SmallLoading/SmallLoading.tsx +++ b/src/components/ui/SmallLoading/SmallLoading.tsx @@ -12,8 +12,8 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import React from "react"; import { observer } from "mobx-react"; +import React from "react"; import styled, { css } from "styled-components"; import { ThemePalette, ThemeProps } from "@src/components/Theme"; @@ -53,8 +53,6 @@ const ProgressText = styled.div` `; const CircleProgressBar = styled.circle``; -export const TEST_ID = "smallLoading"; - export type Props = { loadingProgress: number; }; diff --git a/tests/TestUtils.ts b/tests/TestUtils.ts index c311559c..af335f32 100644 --- a/tests/TestUtils.ts +++ b/tests/TestUtils.ts @@ -1,20 +1,27 @@ -const className = (classNameStartsWith: string, useContains?: boolean) => `[class${useContains ? '*' : '^'}=${classNameStartsWith}]` +const className = (classNameStartsWith: string, useContains?: boolean) => + `[class${useContains ? "*" : "^"}=${classNameStartsWith}]`; export default { - select: (name: string, parent?: Element) => (parent || document).querySelector(className(name)), - selectInput: (name: string, parent?: Element) => (parent || document).querySelector(className(name)), - selectContains: (name: string, parent?: Element) => (parent || document).querySelector(className(name, true)), - selectAll: (name: string, parent?: Element) => (parent || document).querySelectorAll(className(name)), + select: (name: string, parent?: Element) => + (parent || document).querySelector(className(name)), + selectInput: (name: string, parent?: Element) => + (parent || document).querySelector(className(name)), + selectContains: (name: string, parent?: Element) => + (parent || document).querySelector(className(name, true)), + selectAll: (name: string, parent?: Element) => + (parent || document).querySelectorAll(className(name)), rgbToHex: (rgb: string) => { const componentToHex = (c: number) => { - const hex = c.toString(16).toUpperCase() - return hex.length === 1 ? `0${hex}` : hex - } - const matches = /rgb\((\d+), (\d+), (\d+)\)/.exec(rgb) + const hex = c.toString(16).toUpperCase(); + return hex.length === 1 ? `0${hex}` : hex; + }; + const matches = /rgb\((\d+), (\d+), (\d+)\)/.exec(rgb); if (matches) { - const transform = (match: string) => componentToHex(parseInt(match, 10)) - return `#${transform(matches[1])}${transform(matches[2])}${transform(matches[3])}` + const transform = (match: string) => componentToHex(parseInt(match, 10)); + return `#${transform(matches[1])}${transform(matches[2])}${transform( + matches[3] + )}`; } - return rgb + return rgb; }, -} +}; diff --git a/tests/mocks/EndpointsMock.ts b/tests/mocks/EndpointsMock.ts new file mode 100644 index 00000000..2225787c --- /dev/null +++ b/tests/mocks/EndpointsMock.ts @@ -0,0 +1,32 @@ +import { Endpoint } from "@src/@types/Endpoint"; + +export const OPENSTACK_ENDPOINT_MOCK: Endpoint = { + id: "openstack", + name: "OpenStack", + description: "Openstack endpoint", + type: "openstack", + created_at: "2023-11-26T12:00:00Z", + mapped_regions: ["us-east-1"], + connection_info: { + host: "https://api.example.com:1234/path", + username: "admin", + password: "password", + project_name: "admin", + project_domain_name: "Default", + user_domain_name: "Default", + }, +}; + +export const VMWARE_ENDPOINT_MOCK: Endpoint = { + id: "vmware", + name: "VMware", + description: "VMware endpoint", + type: "vmware_vsphere", + created_at: "2023-11-26T12:00:00Z", + mapped_regions: ["us-east-1"], + connection_info: { + host: "https://api.example.com:1234/path", + username: "admin", + password: "password", + }, +}; diff --git a/tests/mocks/ExecutionsMock.ts b/tests/mocks/ExecutionsMock.ts new file mode 100644 index 00000000..0ae229ef --- /dev/null +++ b/tests/mocks/ExecutionsMock.ts @@ -0,0 +1,36 @@ +import { Execution, ExecutionTasks } from "@src/@types/Execution"; +import { ProgressUpdate, Task } from "@src/@types/Task"; + +export const EXECUTION_MOCK: Execution = { + id: "execution-id", + number: 1, + status: "COMPLETED", + created_at: "2023-11-26T12:00:00Z", + updated_at: "2023-11-26T12:00:00Z", + type: "replica_execution", +}; + +export const PROGRESS_UPDATE_MOCK: ProgressUpdate = { + index: 1, + message: "message progress 66%", + created_at: "2023-11-26T12:00:00Z", + total_steps: 1, + current_step: 1, +}; + +export const TASK_MOCK: Task = { + id: "task-id", + status: "COMPLETED", + created_at: "2023-11-26T12:00:00Z", + updated_at: "2023-11-26T12:00:00Z", + progress_updates: [PROGRESS_UPDATE_MOCK], + task_type: "replica_execution", + instance: "instance-id", + depends_on: [], + exception_details: "exception-details", +}; + +export const EXECUTION_TASKS_MOCK: ExecutionTasks = { + ...EXECUTION_MOCK, + tasks: [TASK_MOCK], +}; diff --git a/tests/mocks/InstancesMock.ts b/tests/mocks/InstancesMock.ts new file mode 100644 index 00000000..04e07489 --- /dev/null +++ b/tests/mocks/InstancesMock.ts @@ -0,0 +1,24 @@ +import { Instance } from "@src/@types/Instance"; +import { DISK_MOCK } from "@tests/mocks/StoragesMock"; + +export const INSTANCE_MOCK: Instance = { + id: "instance-id", + name: "instance-name", + flavor_name: "instance-flavor-name", + instance_name: "instance-instance-name", + num_cpu: 1, + memory_mb: 1024, + os_type: "instance-os-type", + devices: { + nics: [ + { + id: "nic-id", + network_name: "network-name", + ip_addresses: ["nic-ip-addresses"], + mac_address: "nic-mac-address", + network_id: "network-id", + }, + ], + disks: [DISK_MOCK], + }, +}; diff --git a/tests/mocks/MetalHubServerMock.ts b/tests/mocks/MetalHubServerMock.ts new file mode 100644 index 00000000..883f334a --- /dev/null +++ b/tests/mocks/MetalHubServerMock.ts @@ -0,0 +1,52 @@ +import { MetalHubServer } from "@src/@types/MetalHub"; + +export const METALHUB_SERVER_MOCK: MetalHubServer = { + id: 12345, + active: true, + hostname: "server01.example.com", + created_at: "2023-11-26T12:00:00Z", + updated_at: "2023-11-26T12:00:00Z", + api_endpoint: "https://api.example.com:1234/path", + firmware_type: "UEFI", + memory: 32768, // in MB + os_info: { + os_name: "Ubuntu", + os_version: "20.04", + }, + disks: [ + { + id: "disk1", + path: "/dev/sda", + name: "Main Disk", + size: 1024000, // in MB + physical_sector_size: 512, + partitions: [ + { + name: "boot", + path: "/dev/sda1", + partition_uuid: "uuid-boot", + sectors: 2048, + start_sector: 0, + end_sector: 2047, + }, + { + name: "root", + path: "/dev/sda2", + sectors: 1021952, + start_sector: 2048, + end_sector: 1024000, + }, + ], + }, + ], + nics: [ + { + interface_type: "Ethernet", + ip_addresses: ["192.168.1.10", "192.168.1.11"], + mac_address: "00:1B:44:11:3A:B7", + nic_name: "eth0", + }, + ], + physical_cores: 8, + logical_cores: 16, +}; diff --git a/tests/mocks/MinionPoolMock.ts b/tests/mocks/MinionPoolMock.ts new file mode 100644 index 00000000..d46b1f80 --- /dev/null +++ b/tests/mocks/MinionPoolMock.ts @@ -0,0 +1,83 @@ +import { + MinionMachine, + MinionPool, + MinionPoolDetails, +} from "@src/@types/MinionPool"; + +export const MINION_MACHINE_MOCK: MinionMachine = { + id: "minion-machine-id", + created_at: "2023-11-26T12:00:00Z", + updated_at: "2023-11-26T12:00:00Z", + allocation_status: "ALLOCATED", + connection_info: {}, + power_status: "on", + provider_properties: {}, + last_used_at: "2023-11-26T12:00:00Z", + allocated_action: "replica-id", +}; + +export const MINION_POOL_MOCK: MinionPool = { + id: "minion-pool-id", + created_at: "2023-11-26T12:00:00Z", + updated_at: "2023-11-26T12:00:00Z", + name: "minion-pool-name", + os_type: "linux", + status: "ACTIVE", + minimum_minions: 1, + maximum_minions: 10, + environment_options: { + option_1: "option_1_value", + object_option: { + object_option_1: "object_option_1_value", + }, + array_option: ["array_option_1_value", "array_option_2_value"], + object_with_mappings: { + mappings: [ + { + source: "source_value", + destination: "destination_value", + }, + ], + }, + }, + endpoint_id: "openstack", + notes: "minion-pool-notes", + platform: "source", + minion_machines: [{ ...MINION_MACHINE_MOCK }], + minion_retention_strategy: "poweroff", + minion_max_idle_time: 0, +}; + +export const MINION_POOL_DETAILS_MOCK: MinionPoolDetails = { + ...MINION_POOL_MOCK, + events: [ + { + id: "minion-pool-event-id", + index: 0, + level: "INFO", + message: "minion-pool-event-message", + created_at: "2023-11-26T12:00:00Z", + }, + { + id: "minion-pool-event-id-2", + index: 1, + level: "DEBUG", + message: "minion-pool-event-message-debug", + created_at: "2023-11-25T12:00:00Z", + }, + ], + progress_updates: [ + { + id: "minion-pool-progress-update-id", + current_step: 0, + message: "minion-pool-progress-update-message", + created_at: "2023-11-26T12:00:00Z", + }, + { + id: "minion-pool-progress-update-2", + current_step: 1, + message: "minion-pool-progress-update-message-2", + created_at: "2023-11-25T12:00:00Z", + }, + ], +}; diff --git a/tests/mocks/NetworksMock.ts b/tests/mocks/NetworksMock.ts new file mode 100644 index 00000000..6d4e1d2d --- /dev/null +++ b/tests/mocks/NetworksMock.ts @@ -0,0 +1,8 @@ +import { Network } from "@src/@types/Network"; + +export const NETWORK_MOCK: Network = { + name: "network-name", + id: "network-id", + security_groups: ["security-group-id"], + port_keys: ["port-key"], +}; diff --git a/tests/mocks/ProvidersMock.ts b/tests/mocks/ProvidersMock.ts new file mode 100644 index 00000000..e6677ba6 --- /dev/null +++ b/tests/mocks/ProvidersMock.ts @@ -0,0 +1,50 @@ +import { Providers } from "@src/@types/Providers"; +import { providerTypes } from "@src/constants"; + +export const PROVIDERS_MOCK: Providers = { + aws: { + types: [], + }, + azure: { + types: [], + }, + openstack: { + types: [providerTypes.DESTINATION_MINION_POOL], + }, + opc: { + types: [], + }, + opca: { + types: [], + }, + oracle_vm: { + types: [], + }, + vmware_vsphere: { + types: [providerTypes.SOURCE_MINION_POOL], + }, + oci: { + types: [], + }, + "hyper-v": { + types: [], + }, + scvmm: { + types: [], + }, + olvm: { + types: [], + }, + kubevirt: { + types: [], + }, + metal: { + types: [], + }, + rhev: { + types: [], + }, + lxd: { + types: [], + }, +}; diff --git a/tests/mocks/SchedulesMock.ts b/tests/mocks/SchedulesMock.ts new file mode 100644 index 00000000..ad0b72b5 --- /dev/null +++ b/tests/mocks/SchedulesMock.ts @@ -0,0 +1,15 @@ +import { DateTime } from "luxon"; + +import { Schedule } from "@src/@types/Schedule"; + +export const SCHEDULE_MOCK: Schedule = { + id: "schedule-1", + enabled: true, + schedule: { + hour: 1, + dow: 1, + dom: 1, + }, + expiration_date: DateTime.now().plus({ years: 1 }).toJSDate(), + shutdown_instances: true, +}; diff --git a/tests/mocks/StoragesMock.ts b/tests/mocks/StoragesMock.ts new file mode 100644 index 00000000..6623a923 --- /dev/null +++ b/tests/mocks/StoragesMock.ts @@ -0,0 +1,23 @@ +import { StorageBackend } from "@src/@types/Endpoint"; +import { Disk } from "@src/@types/Instance"; + +export const DISK_MOCK: Disk = { + id: "disk-id", + name: "disk-name", + storage_backend_identifier: "disk-storage-backend-identifier", + format: "disk-format", + guest_device: "disk-guest-device", + size_bytes: 1024, + disabled: { + message: "disk-disabled-message", + info: "disk-disabled-info", + }, +}; + +export const STORAGE_BACKEND_MOCK: StorageBackend = { + id: "storage-backend-id", + name: "storage-backend-name", + additional_provider_properties: { + supported_bus_types: ["storage-backend-supported-bus-types"], + }, +}; diff --git a/tests/mocks/TransferMock.ts b/tests/mocks/TransferMock.ts new file mode 100644 index 00000000..a6c82495 --- /dev/null +++ b/tests/mocks/TransferMock.ts @@ -0,0 +1,97 @@ +import { + MigrationItem, + MigrationItemDetails, + ReplicaItem, + ReplicaItemDetails, +} from "@src/@types/MainItem"; +import { EXECUTION_MOCK, TASK_MOCK } from "@tests/mocks/ExecutionsMock"; +import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock"; + +export const REPLICA_MOCK: ReplicaItem = { + id: "replica-id", + name: "replica-name", + type: "replica", + description: "replica-description", + notes: "replica-notes", + created_at: "2023-11-26T12:00:00Z", + updated_at: "2023-11-26T12:00:00Z", + origin_endpoint_id: "vmware", + destination_endpoint_id: "openstack", + origin_minion_pool_id: "origin-minion-pool-id", + destination_minion_pool_id: "destination-minion-pool-id", + instances: ["instance-id"], + info: {}, + destination_environment: { + option_1: "option_1_value", + object_option: { + object_option_1: "object_option_1_value", + }, + array_option: ["array_option_1_value", "array_option_2_value"], + object_with_mappings: { + mappings: [ + { + source: "source_value", + destination: "destination_value", + }, + ], + disk_mappings: {}, + }, + password: "password-value", + }, + source_environment: {}, + transfer_result: { + "instance-id": { ...INSTANCE_MOCK }, + }, + last_execution_status: "COMPLETED", + user_id: "user-id", + network_map: { + // @ts-ignore + "network-name": "network-name", + }, + storage_mappings: { + backend_mappings: [ + { + destination: "destination_value", + source: "source_value", + }, + ], + default: "default_value", + disk_mappings: [ + { + destination: "destination_value", + disk_id: "disk_id_value", + }, + ], + }, +}; + +export const REPLICA_ITEM_DETAILS_MOCK: ReplicaItemDetails = { + ...REPLICA_MOCK, + executions: [EXECUTION_MOCK], +}; + +export const MIGRATION_MOCK: MigrationItem = { + id: "migration-id", + name: "migration-name", + type: "migration", + description: "migration-description", + notes: "migration-notes", + created_at: "2023-11-26T12:00:00Z", + updated_at: "2023-11-26T12:00:00Z", + origin_endpoint_id: "openstack", + destination_endpoint_id: "vmware", + origin_minion_pool_id: "origin-minion-pool-id", + destination_minion_pool_id: "destination-minion-pool-id", + instances: ["instance-id"], + info: {}, + destination_environment: {}, + source_environment: {}, + transfer_result: {}, + last_execution_status: "COMPLETED", + user_id: "user-id", +}; + +export const MIGRATION_ITEM_DETAILS_MOCK: MigrationItemDetails = { + ...MIGRATION_MOCK, + tasks: [{ ...TASK_MOCK, task_type: "migration_task" }], +}; diff --git a/tests/mocks/UsersMock.ts b/tests/mocks/UsersMock.ts new file mode 100644 index 00000000..86dc2bb3 --- /dev/null +++ b/tests/mocks/UsersMock.ts @@ -0,0 +1,17 @@ +import { User } from "@src/@types/User"; + +export const USER_MOCK: User = { + scoped: true, + project: { + id: "project-id", + name: "project-name", + }, + email: "user-email", + name: "user-name", + id: "user-id", + description: "user-description", + enabled: true, + project_id: "project-id", + domain_id: "domain-id", + isAdmin: true, +}; diff --git a/tests/setup.js b/tests/setup.js index 2b2db689..9865dc88 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -1,5 +1,12 @@ -window.scroll = jest.fn() +import "jest-canvas-mock"; -const originalErr = console.error.bind(console.error) -// @TODO fix components which throw these warnings -console.error = (...args) => !args[0].toString().includes('Unknown event handler property') && originalErr(...args) +window.scroll = jest.fn(); +const originalWarn = console.warn.bind(console.warn); +console.warn = (...args) => + !args[0].toString().includes("observer class") && originalWarn(...args); + +const originalErr = console.error.bind(console.error); +console.error = (...args) => + // @TODO fix components which throw these warnings + !args[0].toString().includes("Unknown event handler property") && + originalErr(...args); diff --git a/tests/testCoverage.js b/tests/testCoverage.js deleted file mode 100644 index 3a5d750c..00000000 --- a/tests/testCoverage.js +++ /dev/null @@ -1,62 +0,0 @@ -const fs = require('fs') - -const SKIP_FOLDERS = [ - { - pattern: 'smart', - reason: 'No smart components testing yet', - }, - { - pattern: 'AssessmentModule', - reason: 'Assessment module is not in use for now', - }, -] - -const main = async () => { - let coveredComponents = 0 - let uncoveredComponents = 0 - let skippedComponents = 0 - const skippedMessageLog = [] - - const readDir = async dir => { - const exists = fs.existsSync(`${dir}/package.json`) - if (exists) { - const skippedPattern = SKIP_FOLDERS.find(skip => dir.includes(`/${skip.pattern}/`)) - if (skippedPattern) { - skippedComponents += 1 - if (!skippedMessageLog.find(skip => skip.pattern === skippedPattern.pattern)) { - skippedMessageLog.push(skippedPattern) - } - return - } - - const files = await fs.promises.readdir(dir) - const tsxFiles = files.filter(file => file.endsWith('.tsx') && !file.endsWith('.spec.tsx')) - const specFiles = files.filter(file => file.endsWith('.spec.tsx')) - - if (tsxFiles.length > 0 && specFiles.length === 0) { - uncoveredComponents += 1 - console.log(`\x1b[31m${dir}\x1b[0m`) - } else if (tsxFiles.length > 0 && specFiles.length > 0) { - coveredComponents += 1 - } - } else { - const files = await fs.promises.readdir(dir, { withFileTypes: true }) - for (const file of files) { - if (file.isDirectory()) { - await readDir(`${dir}/${file.name}`) - } - } - } - } - - await readDir('./src') - - skippedMessageLog.forEach(skip => console.log(`Skipping '${skip.pattern}' pattern. ${skip.reason}`)) - - const percentage = (coveredComponents / (coveredComponents + uncoveredComponents)) * 100 - console.log(`\nSkipped components: ${skippedComponents}`) - console.log(`Covered components: ${coveredComponents} / ${coveredComponents + uncoveredComponents}`) - console.log(`\x1b[34mCoverage: ${percentage.toFixed(2)} %\x1b[0m`) -} - -main() diff --git a/yarn.lock b/yarn.lock index 980b2b12..5821746f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3461,10 +3461,10 @@ __metadata: languageName: node linkType: hard -"@types/luxon@npm:^3.3.2": - version: 3.3.2 - resolution: "@types/luxon@npm:3.3.2" - checksum: b9111132720eae0269538872a5a496b29587ecfc8edc3b0ff7d269aa93a5ff00a131b23d1e9d1f12ec39f2c779ad21bd8d9f90b122c85a182771aabde7f676b8 +"@types/luxon@npm:^3.3.3": + version: 3.3.3 + resolution: "@types/luxon@npm:3.3.3" + checksum: 072dd39eea3f63453788fab2fcfc83eb33917afcaffe178ce08ecd8b016824b8ab3bfa991f66266f2fc1927768a56b4334945f2eb1d83638e325c0c43d7d0e86 languageName: node linkType: hard @@ -6317,7 +6317,7 @@ __metadata: languageName: node linkType: hard -"color-name@npm:~1.1.4": +"color-name@npm:^1.1.4, color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 @@ -6635,7 +6635,7 @@ __metadata: "@types/file-saver": ^2.0.1 "@types/jest": ^27.0.2 "@types/js-cookie": ^2.2.6 - "@types/luxon": ^3.3.2 + "@types/luxon": ^3.3.3 "@types/moment-timezone": ^0.5.13 "@types/react": ^16.13.1 "@types/react-collapse": ^5.0.0 @@ -6669,6 +6669,7 @@ __metadata: fs: ^0.0.1-security html-webpack-plugin: ^3.2.0 jest: ^27.3.1 + jest-canvas-mock: ^2.5.2 js-base64: ^3.5.2 js-cookie: ^2.2.1 jszip: ^3.8.0 @@ -6919,6 +6920,13 @@ __metadata: languageName: node linkType: hard +"cssfontparser@npm:^1.2.1": + version: 1.2.1 + resolution: "cssfontparser@npm:1.2.1" + checksum: 952d487cddab591fb944f2a4c326a7736bc963784a6d92b6ad4051f3bf5ee49a732eff62e29a52ff085197cb07f5bd66525a2245ded7fd356113ac81be9238b9 + languageName: node + linkType: hard + "cssom@npm:^0.4.4": version: 0.4.4 resolution: "cssom@npm:0.4.4" @@ -7990,8 +7998,8 @@ __metadata: "eslint-plugin-coriolis-web@file:./src/utils/eslint-plugin-coriolis-web::locator=coriolis-web%40workspace%3A.": version: 0.0.0 - resolution: "eslint-plugin-coriolis-web@file:./src/utils/eslint-plugin-coriolis-web#./src/utils/eslint-plugin-coriolis-web::hash=2f2a4c&locator=coriolis-web%40workspace%3A." - checksum: 53270112c732af777cdf8044e3f0c8af4bf377506dd298d924583c79635991d785ed4831e38cb3d4f55d311f0918ef7048c051a147d39f854308b85362f2c300 + resolution: "eslint-plugin-coriolis-web@file:./src/utils/eslint-plugin-coriolis-web#./src/utils/eslint-plugin-coriolis-web::hash=11b726&locator=coriolis-web%40workspace%3A." + checksum: bb3a1dd0feae7faeac9e86b3b3be805a73ca7019130839e2fc5fa247276ddb8a3fd9c1fcca7390cdf1d65201483300df4beb8767c1de7e0d843aad494105c11a languageName: node linkType: hard @@ -11054,6 +11062,16 @@ __metadata: languageName: node linkType: hard +"jest-canvas-mock@npm:^2.5.2": + version: 2.5.2 + resolution: "jest-canvas-mock@npm:2.5.2" + dependencies: + cssfontparser: ^1.2.1 + moo-color: ^1.0.2 + checksum: a3004d2e96473049045e49dcf98e5ea6011494048ab42b5422b3089d9ff406aaca8353e79587055d840fa145541668eb8f78613765f252ad5901a8217e91ea5d + languageName: node + linkType: hard + "jest-changed-files@npm:^27.5.1": version: 27.5.1 resolution: "jest-changed-files@npm:27.5.1" @@ -12778,6 +12796,15 @@ __metadata: languageName: node linkType: hard +"moo-color@npm:^1.0.2": + version: 1.0.3 + resolution: "moo-color@npm:1.0.3" + dependencies: + color-name: ^1.1.4 + checksum: 02bf59b6bbd5e86641bc062e2dc0843e6e579e18ef67e1c8e93bfc01945df578f20e66ce16aa9632db2aa0e16806e0914a26eb345a804f45fff1ae12a8906a29 + languageName: node + linkType: hard + "move-concurrently@npm:^1.0.1": version: 1.0.1 resolution: "move-concurrently@npm:1.0.1"