From 8b2bb662fea0759efa7754818f072a7ed39347af Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Tue, 27 Feb 2024 15:51:46 +0100 Subject: [PATCH] BC-3844 - upgrade to Vue3 and Vuetify3 (#2789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To be able to use the latest features of Vue we want to upgrade the frontend to Vue 3. Doing that we have to upgrade to Vuetify 3. --------- Co-authored-by: NFriedo <69233063+NFriedo@users.noreply.github.com> Co-authored-by: odalys-dataport <82401838+odalys-dataport@users.noreply.github.com> Co-authored-by: Christian Darsow <104998795+christian-darsow@users.noreply.github.com> Co-authored-by: Odalys Adam Co-authored-by: Oliver Happe Co-authored-by: hoeppner-dataport Co-authored-by: Murat Merdoglu Co-authored-by: hoeppner-dataport <106819770+hoeppner-dataport@users.noreply.github.com> Co-authored-by: Martin Schuhmacher <55735359+MartinSchuhmacher@users.noreply.github.com> Co-authored-by: Murat Merdoglu <64781656+muratmerdoglu-dp@users.noreply.github.com> Co-authored-by: Max Bischof Co-authored-by: Max Bischof <106820326+bischofmax@users.noreply.github.com> Co-authored-by: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> Co-authored-by: Omar Ezzat Co-authored-by: KalliSfak <127407169+KalliSfak@users.noreply.github.com> Co-authored-by: ezzato Co-authored-by: Oliver Happe <108657965+OliverHappe@users.noreply.github.com> Co-authored-by: agnisa-cap Co-authored-by: virgilchiriac <17074330+virgilchiriac@users.noreply.github.com> Co-authored-by: bergatco <129839305+bergatco@users.noreply.github.com> Co-authored-by: Marvin Öhlerking <103562092+MarvinOehlerkingCap@users.noreply.github.com> Co-authored-by: odalys-dataport Co-authored-by: virgilchiriac Co-authored-by: MartinSchuhmacher --- .eslintrc.js | 8 +- .github/workflows/dependency-review.yml | 8 +- .prettierignore | 1 + VUE3-UPGRADE-RESULTS/BREAKING-CHANGES.md | 21 + VUE3-UPGRADE-RESULTS/BUILD.md | 17 + VUE3-UPGRADE-RESULTS/DATA-TABLES.md | 9 + VUE3-UPGRADE-RESULTS/DEPENDENCY-UPGRADES.md | 71 + VUE3-UPGRADE-RESULTS/ESTIMATION.md | 17 + VUE3-UPGRADE-RESULTS/FilesToBeRefactored.md | 85 + VUE3-UPGRADE-RESULTS/STORE-REFACTORING.md | 21 + VUE3-UPGRADE-RESULTS/TESTING.md | 61 + VUE3-UPGRADE-RESULTS/VUE3-UPGRADE.md | 28 + VUE3-UPGRADE-RESULTS/VUETIFY-UPGRADE.md | 262 + jest.config.js | 40 +- jsconfig.json | 176 - package-lock.json | 24641 +++------------- package.json | 67 +- src/App.vue | 5 +- .../AdminMigrationSection.unit.ts | 284 +- .../administration/AdminMigrationSection.vue | 35 +- .../ExternalToolSection.unit.ts | 207 +- .../administration/ExternalToolSection.vue | 38 +- .../ExternalToolToolbar.unit.ts | 40 +- .../administration/ExternalToolToolbar.vue | 20 +- .../MigrationWarningCard.unit.ts | 134 +- .../administration/MigrationWarningCard.vue | 7 +- .../ProvisioningOptionsPage.unit.ts | 102 +- .../ProvisioningOptionsPage.vue | 52 +- .../external-tool-section-utils.composable.ts | 13 +- ...rnal-tool-section-utils.composable.unit.ts | 38 +- src/components/atoms/InfoMessage.unit.js | 32 - src/components/atoms/InfoMessage.unit.ts | 34 + src/components/atoms/InfoMessage.vue | 13 +- .../atoms/VCustomChipTimeRemaining.vue | 11 +- src/components/atoms/vCustomAutocomplete.vue | 23 - .../atoms/vCustomBreadcrumbs.unit.ts | 39 +- src/components/atoms/vCustomBreadcrumbs.vue | 28 +- .../atoms/vCustomChipTimeRemaining.unit.js | 117 - .../atoms/vCustomChipTimeRemaining.unit.ts | 116 + src/components/atoms/vCustomFab.unit.ts | 196 - src/components/atoms/vCustomFab.vue | 229 - src/components/atoms/vCustomSwitch.unit.ts | 107 - src/components/atoms/vCustomSwitch.vue | 42 - src/components/atoms/vRoomAvatar.unit.ts | 280 +- src/components/atoms/vRoomAvatar.vue | 59 +- .../atoms/vRoomDeleteAvatar.unit.ts | 25 +- src/components/atoms/vRoomDeleteAvatar.vue | 4 +- src/components/atoms/vRoomEmptyAvatar.unit.ts | 57 +- src/components/atoms/vRoomEmptyAvatar.vue | 15 +- .../base/BaseDialog/BaseDialog.unit.js | 124 +- src/components/base/BaseDialog/BaseDialog.vue | 31 +- .../base/BaseInput/BaseInput.unit.js | 84 +- src/components/base/BaseInput/BaseInput.vue | 29 +- .../base/BaseInput/BaseInputCheckbox.unit.js | 130 +- .../base/BaseInput/BaseInputCheckbox.vue | 26 +- .../base/BaseInput/BaseInputDefault.unit.js | 115 +- .../base/BaseInput/BaseInputDefault.vue | 60 +- .../base/BaseInput/BaseInputHidden.unit.js | 2 +- .../base/BaseInput/BaseInputHidden.vue | 10 +- .../base/BaseInput/BaseInputRadio.unit.js | 56 - .../base/BaseInput/BaseInputRadio.vue | 14 +- src/components/base/BaseLink.unit.js | 117 +- src/components/base/BaseLink.vue | 14 +- src/components/base/BaseModal.unit.js | 87 +- src/components/base/BaseModal.vue | 196 +- src/components/base/_globals.js | 51 - src/components/base/components.js | 17 + .../copy-result-modal/CopyResultModal.unit.ts | 165 +- .../copy-result-modal/CopyResultModal.vue | 18 +- .../CopyResultModalList.unit.ts | 31 +- .../CopyResultModalListItem.unit.ts | 54 +- .../CopyResultModalListItem.vue | 4 +- .../BoardPageInformation.composable.ts | 23 +- .../BoardPageInformation.composable.unit.ts | 32 +- .../data-board/BoardState.composable.ts | 197 +- .../data-board/BoardState.composable.unit.ts | 191 +- .../data-board/CardState.composable.unit.ts | 30 +- .../ContentElementState.composable.ts | 4 +- .../data-board/ContentElementState.unit.ts | 16 +- ...ToolConfigurationStatus.composable.unit.ts | 50 +- ...ExternalToolLaunchState.composable.unit.ts | 8 + .../data-group/GroupState.composable.ts | 2 +- .../data-group/GroupState.composable.unit.ts | 13 +- .../ProvisioningOptionsApi.composable.unit.ts | 16 +- .../ProvisioningOptionsState.composable.ts | 2 +- ...rovisioningOptionsState.composable.unit.ts | 57 +- .../error-handling/ErrorContent.vue | 6 +- .../error-handling/ErrorHandler.composable.ts | 2 +- .../ErrorHandler.composable.unit.ts | 39 +- .../error-handling/errorContent.unit.ts | 30 +- .../ExternalToolConfigParameter.unit.ts | 57 +- .../ExternalToolConfigParameter.vue | 135 +- .../ExternalToolConfigSettings.unit.ts | 30 +- .../ExternalToolConfigSettings.vue | 38 +- .../ExternalToolConfigurator.unit.ts | 60 +- .../ExternalToolConfigurator.vue | 34 +- .../ExternalToolSelectionRow.unit.ts | 45 +- .../ExternalToolSelectionRow.vue | 23 +- .../external-tool-validation.composable.ts | 8 +- ...xternal-tool-validation.composable.unit.ts | 10 +- .../DrawingContentElement.unit.ts | 47 +- .../DrawingContentElement.vue | 13 +- .../InnerContent.unit.ts | 24 +- .../InnerContent.vue | 2 +- .../ExternalToolElement.unit.ts | 103 +- .../ExternalToolElement.vue | 12 +- .../ExternalToolElementAlert.unit.ts | 65 +- .../ExternalToolElementAlert.vue | 7 +- ...rnalToolElementConfigurationDialog.unit.ts | 47 +- ...ExternalToolElementConfigurationDialog.vue | 12 +- .../ExternalToolElementMenu.unit.ts | 29 +- .../ExternalToolElementMenu.vue | 7 +- .../FileContentElement.unit.ts | 122 +- .../FileContentElement.vue | 12 +- .../content/FileContent.unit.ts | 10 +- .../content/alert/FileAlerts.unit.ts | 14 +- .../content/alert/FileAlerts.vue | 2 +- .../content/display/FileDisplay.unit.ts | 107 +- .../audio-display/AudioDisplay.unit.ts | 54 +- .../display/audio-display/AudioDisplay.vue | 36 +- .../display/audio-display/SpeedMenu.unit.ts | 44 +- .../display/audio-display/SpeedMenu.vue | 152 +- .../file-description/FileDescription.unit.ts | 60 +- .../file-description/FileDescription.vue | 7 +- .../image-display/ImageDisplay.unit.ts | 68 +- .../display/image-display/ImageDisplay.vue | 55 +- .../display/pdf-display/PdfDisplay.unit.ts | 39 +- .../display/pdf-display/PdfDisplay.vue | 50 +- .../video-display/VideoDisplay.unit.ts | 9 +- .../display/video-display/VideoDisplay.vue | 3 +- .../footer/ContentElementFooter.unit.ts | 9 +- .../footer/attributes/FileAttributes.unit.ts | 36 +- .../footer/attributes/FileAttributes.vue | 6 +- .../footer/download/FileDownload.unit.ts | 47 +- .../content/footer/download/FileDownload.vue | 4 +- .../content/inputs/FileInputs.unit.ts | 10 +- .../content/inputs/FileInputs.vue | 2 +- .../alternative-text/AlternativeText.unit.ts | 36 +- .../alternative-text/AlternativeText.vue | 1 - .../inputs/caption/CaptionText.unit.ts | 36 +- .../content/inputs/caption/CaptionText.vue | 2 +- .../FileStorageApi.composable.unit.ts | 13 +- .../FileStorageNotifications.composable.ts | 30 +- ...ileStorageNotifications.composable.unit.ts | 42 +- .../upload/FileUpload.unit.ts | 87 +- .../upload/FileUpload.vue | 40 +- .../upload/file-picker/FilePicker.unit.ts | 38 +- .../upload/file-picker/FilePicker.vue | 17 +- .../components/LinkContentElement.unit.ts | 83 +- .../components/LinkContentElement.vue | 193 +- .../LinkContentElementCreate.unit.ts | 145 +- .../components/LinkContentElementCreate.vue | 97 +- .../LinkContentElementDisplay.unit.ts | 28 +- .../components/LinkContentElementDisplay.vue | 10 +- .../MetaTagExtractorApi.composable.ts | 2 +- .../MetaTagExtractorApi.composable.unit.ts | 16 +- .../SubmissionContentElement.unit.ts | 58 +- .../components/SubmissionContentElement.vue | 11 +- .../SubmissionContentElementDisplay.unit.ts | 33 +- .../SubmissionContentElementDisplay.vue | 6 +- .../SubmissionContentElementEdit.unit.ts | 37 +- .../SubmissionContentElementEdit.vue | 6 +- .../SubmissionContentElementTitle.unit.ts | 26 +- .../SubmissionContentElementTitle.vue | 6 +- .../SubmissionItemStudentDisplay.unit.ts | 37 +- .../SubmissionItemStudentDisplay.vue | 3 +- .../SubmissionItemsTeacherDisplay.unit.ts | 67 +- .../SubmissionItemsTeacherDisplay.vue | 126 +- ...sionContentElementState.composable.unit.ts | 13 +- .../RichTextContentElement.unit.ts | 45 +- .../RichTextContentElement.vue | 2 +- .../RichTextContentElementDisplay.unit.ts | 35 +- .../RichTextContentElementEdit.unit.ts | 46 +- .../RichTextContentElementEdit.vue | 4 +- .../feature-board/board/Board.unit.ts | 299 +- src/components/feature-board/board/Board.vue | 197 +- .../board/BoardAddCardButton.unit.ts | 20 +- .../board/BoardAddCardButton.vue | 4 +- .../feature-board/board/BoardColumn.unit.ts | 173 +- .../feature-board/board/BoardColumn.vue | 169 +- .../board/BoardColumnGhost.unit.ts | 66 +- .../feature-board/board/BoardColumnGhost.vue | 96 +- .../board/BoardColumnGhostHeader.unit.ts | 22 +- .../board/BoardColumnGhostHeader.vue | 14 +- .../board/BoardColumnHeader.unit.ts | 31 +- .../feature-board/board/BoardColumnHeader.vue | 33 +- .../BoardColumnInteractionHandler.unit.ts | 18 +- .../card/CardAddElementMenu.unit.ts | 20 +- .../feature-board/card/CardAddElementMenu.vue | 3 +- .../feature-board/card/CardHost.unit.ts | 40 +- .../feature-board/card/CardHost.vue | 17 +- .../card/CardHostDetailView.unit.ts | 23 +- .../feature-board/card/CardHostDetailView.vue | 4 +- .../card/CardHostInteractionHandler.unit.ts | 17 +- .../card/CardHostInteractionHandler.vue | 1 - .../feature-board/card/CardSkeleton.unit.ts | 18 +- .../feature-board/card/CardTitle.unit.ts | 26 +- .../card/ContentElementList.unit.ts | 42 +- .../feature-board/card/ContentElementList.vue | 8 +- .../shared/AddElementDialog.unit.ts | 45 +- .../feature-board/shared/AddElementDialog.vue | 19 +- .../shared/BoardAnyTitleInput.unit.ts | 23 +- .../shared/BoardAnyTitleInput.vue | 93 +- .../InlineEditInteractionHandler.unit.ts | 27 +- .../shared/InlineEditInteractionHandler.vue | 30 +- .../feature-editor/CKEditor.unit.ts | 67 +- src/components/feature-editor/CKEditor.vue | 26 +- .../FormActions.unit.js | 0 .../FormActions.vue | 0 .../FormNews.unit.ts | 100 +- .../FormNews.vue | 36 +- .../TitleInput.unit.js | 0 .../feature-news-form/TitleInput.vue | 29 + src/components/feature-news-form/index.ts | 3 + .../feature-render-html/RenderHTML.unit.ts | 57 +- src/components/h5p/H5PEditor.unit.ts | 28 +- src/components/h5p/H5PEditor.vue | 6 +- src/components/h5p/H5PPlayer.unit.ts | 31 +- src/components/icons/custom/index.ts | 187 +- src/components/icons/material/index.ts | 12 +- src/components/legacy/NavigationBar.unit.js | 11 +- src/components/legacy/NavigationBar.vue | 26 +- src/components/legacy/TheFooter.unit.js | 35 +- src/components/legacy/TheFooter.vue | 34 +- src/components/legacy/TheSidebar.unit.js | 91 - src/components/legacy/TheSidebar.unit.ts | 96 + src/components/legacy/TheSidebar.vue | 20 +- .../AddContentButton.vue | 35 +- .../AddContentModal.unit.ts} | 113 +- .../AddContentModal.vue | 62 +- src/components/lern-store/ContentCard.unit.js | 123 + .../{organisms => lern-store}/ContentCard.vue | 33 +- .../ContentEduSharingFooter.unit.ts | 34 + .../ContentEduSharingFooter.vue | 0 .../lern-store/ContentEmptyState.unit.ts | 56 + .../ContentEmptyState.vue | 9 +- .../lern-store/ContentInitialState.unit.ts | 25 + .../ContentInitialState.vue | 13 +- .../lern-store/LernStoreGrid.unit.ts | 13 +- src/components/lern-store/LernStoreGrid.vue | 3 - .../lern-store/LernStorePlayer.unit.js | 45 +- .../LernstoreCollectionDetailView.unit.js | 50 +- .../LernstoreCollectionDetailView.vue | 22 +- .../lern-store/LernstoreDetailView.unit.js | 27 +- .../lern-store/LernstoreDetailView.vue | 39 +- .../lern-store/LoadingModal.unit.ts | 29 + .../LoadingModal.vue | 23 +- .../NotificationModal.unit.ts} | 44 +- .../NotificationModal.vue | 10 +- src/components/molecules/AdminTableLegend.vue | 4 +- src/components/molecules/Alert.unit.ts | 71 +- src/components/molecules/Alert.vue | 71 +- .../molecules/ApplicationErrorRouting.unit.ts | 43 +- .../molecules/ApplicationErrorRouting.vue | 7 +- .../molecules/ApplicationErrorWrapper.vue | 9 +- .../molecules/ContentEduSharingFooter.unit.js | 23 - .../molecules/ContentEmptyState.unit.js | 21 - .../molecules/ContentInitialState.unit.js | 21 - .../molecules/ContentSearchbar.unit.js | 50 - src/components/molecules/ContentSearchbar.vue | 176 - ...ontextMenu.unit.js => ContextMenu.unit.ts} | 93 +- src/components/molecules/ContextMenu.vue | 20 +- src/components/molecules/EmptyState.unit.js | 34 - src/components/molecules/EmptyState.vue | 407 - src/components/molecules/ImportModal.unit.ts | 288 - src/components/molecules/ImportModal.vue | 222 - src/components/molecules/InfoBox.unit.js | 12 - src/components/molecules/InfoBox.unit.ts | 82 + src/components/molecules/InfoBox.vue | 8 +- .../molecules/InfoModalFullWidth.vue | 11 +- src/components/molecules/LoadingModal.unit.js | 21 - .../molecules/LoadingStateDialog.unit.ts | 26 +- .../molecules/LoadingStateDialog.vue | 4 +- src/components/molecules/ModalBodyInfo.vue | 1 + src/components/molecules/ModalFooter.vue | 6 +- .../molecules/ModalFooterActions.vue | 2 +- .../molecules/ModalFooterConfirm.vue | 13 +- ...essModal.unit.js => ProgressModal.unit.ts} | 11 +- src/components/molecules/ProgressModal.vue | 11 +- .../molecules/RoomBoardCard.unit.ts | 73 +- src/components/molecules/RoomBoardCard.vue | 84 +- src/components/molecules/RoomDotMenu.unit.ts | 71 +- src/components/molecules/RoomDotMenu.vue | 95 +- .../molecules/RoomLessonCard.unit.ts | 171 +- src/components/molecules/RoomLessonCard.vue | 22 +- src/components/molecules/RoomModal.unit.ts | 46 +- src/components/molecules/RoomModal.vue | 42 +- src/components/molecules/RoomTaskCard.unit.ts | 192 +- src/components/molecules/RoomTaskCard.vue | 35 +- src/components/molecules/SkipLinks.unit.js | 23 - src/components/molecules/SkipLinks.unit.ts | 30 + src/components/molecules/SkipLinks.vue | 14 +- ...kItemMenu.unit.js => TaskItemMenu.unit.ts} | 97 +- src/components/molecules/TaskItemMenu.vue | 259 +- ...tudent.unit.js => TaskItemStudent.unit.ts} | 64 +- src/components/molecules/TaskItemStudent.vue | 106 +- ...eacher.unit.js => TaskItemTeacher.unit.ts} | 121 +- src/components/molecules/TaskItemTeacher.vue | 215 +- src/components/molecules/TitleInput.vue | 28 - ...ls.unit.js => vCustomDoublePanels.unit.ts} | 143 +- .../molecules/vCustomDoublePanels.vue | 34 +- ...tate.unit.js => vCustomEmptyState.unit.ts} | 43 +- .../molecules/vCustomEmptyState.vue | 323 +- .../molecules/vImportUsersMatchSearch.unit.ts | 103 +- .../molecules/vImportUsersMatchSearch.vue | 235 +- .../molecules/vRoomGroupAvatar.unit.ts | 61 +- src/components/molecules/vRoomGroupAvatar.vue | 26 +- .../organisms/AutoLogoutWarning.unit.js | 222 +- .../organisms/AutoLogoutWarning.vue | 39 +- src/components/organisms/ContentCard.unit.js | 83 - .../organisms/DataFilter/DataFilter.md | 65 - .../organisms/DataFilter/DataFilter.unit.ts | 125 + .../organisms/DataFilter/DataFilter.vue | 265 +- .../organisms/DataFilter/DataFilterChips.vue | 151 - .../DataFilter/DataFilterLayout.unit.js | 5 - .../organisms/DataFilter/DataFilterLayout.vue | 21 - .../organisms/DataFilter/DataFilterModal.vue | 47 - .../organisms/DataFilter/DataFilterSelect.vue | 83 - .../organisms/DataFilter/FilterDialog.unit.ts | 40 + .../organisms/DataFilter/FilterDialog.vue | 32 + .../composables/filter.composable.ts | 208 + .../composables/filter.composable.unit.ts | 283 + .../composables/localStorage.composable.ts | 56 + .../localStorage.composable.unit.ts | 67 + .../organisms/DataFilter/defaultFilters.js | 93 - .../DataFilter/defaultFilters.unit.js | 140 - .../filterComponents/DateBetween.unit.ts | 145 + .../filterComponents/DateBetween.vue | 83 + .../FilterActionButton.unit.ts | 46 + .../filterComponents/FilterActionButtons.vue | 46 + .../filterComponents/FilterChips.unit.ts | 59 + .../filterComponents/FilterChips.vue | 39 + .../filterComponents/ListSelection.unit.ts | 139 + .../filterComponents/ListSelection.vue | 61 + .../DataFilter/filterComponents/index.ts | 6 + .../DataFilter/inputs/Checkbox.unit.js | 93 - .../organisms/DataFilter/inputs/Checkbox.vue | 82 - .../DataFilter/inputs/Default.unit.js | 3 - .../organisms/DataFilter/inputs/Default.vue | 6 - .../organisms/DataFilter/inputs/Inputs.md | 15 - .../organisms/DataFilter/inputs/Radio.unit.js | 59 - .../organisms/DataFilter/inputs/Radio.vue | 78 - .../organisms/DataFilter/types/enums.ts | 20 + .../organisms/DataFilter/types/index.ts | 27 + .../organisms/DataFilter/types/types.ts | 68 + .../DataTable/BackendDataTable.unit.js | 207 +- .../organisms/DataTable/BackendDataTable.vue | 79 +- .../DataTable/DataTable.data-factory.js | 9 - .../organisms/DataTable/DataTable.md | 72 - .../organisms/DataTable/DataTable.unit.js | 588 - .../organisms/DataTable/DataTable.vue | 180 - .../organisms/DataTable/RowSelectionBar.vue | 22 +- .../organisms/DataTable/TableDataRow.vue | 22 +- .../organisms/DataTable/TableHeadRow.vue | 41 +- .../organisms/FormCreateUser.unit.js | 104 +- src/components/organisms/FormCreateUser.vue | 114 +- .../organisms/Ldap/LdapClassesSection.unit.js | 189 +- .../organisms/Ldap/LdapClassesSection.vue | 65 +- .../Ldap/LdapConnectionSection.unit.js | 138 +- .../organisms/Ldap/LdapConnectionSection.vue | 56 +- .../organisms/Ldap/LdapRolesSection.unit.js | 162 +- .../organisms/Ldap/LdapRolesSection.vue | 107 +- .../organisms/Ldap/LdapUsersSection.unit.js | 145 +- .../organisms/Ldap/LdapUsersSection.vue | 69 +- src/components/organisms/Pagination.unit.js | 90 +- src/components/organisms/Pagination.vue | 24 +- .../organisms/RoomAvatarIterator.unit.ts | 52 +- .../organisms/RoomAvatarIterator.vue | 72 +- src/components/organisms/StepProgress.unit.js | 17 +- src/components/organisms/StepProgress.vue | 18 +- src/components/organisms/TasksList.unit.ts | 95 +- src/components/organisms/TasksList.vue | 18 +- .../administration/AuthSystems.unit.js | 258 +- .../organisms/administration/AuthSystems.vue | 36 +- .../administration/GeneralSettings.unit.js | 167 +- .../administration/GeneralSettings.vue | 54 +- .../administration/ImportUsers.unit.ts | 254 +- .../organisms/administration/ImportUsers.vue | 211 +- .../administration/PrivacySettings.unit.js | 456 +- .../administration/PrivacySettings.vue | 147 +- .../administration/SchoolPolicy.unit.ts | 45 +- .../organisms/administration/SchoolPolicy.vue | 134 +- .../SchoolPolicyFormDialog.unit.ts | 44 +- .../administration/SchoolPolicyFormDialog.vue | 54 +- .../administration/SchoolTerms.unit.ts | 47 +- .../organisms/administration/SchoolTerms.vue | 119 +- .../SchoolTermsFormDialog.unit.ts | 44 +- .../administration/SchoolTermsFormDialog.vue | 48 +- .../organisms/vCustomDialog.unit.js | 182 - .../organisms/vCustomDialog.unit.ts | 166 + src/components/organisms/vCustomDialog.vue | 164 +- .../page-board/ColumnBoard.page.unit.ts | 19 +- .../page-board/ColumnBoard.page.vue | 7 +- .../ClassMembers.page.unit.ts | 144 +- .../page-class-members/ClassMembers.page.vue | 27 +- .../ClassMembersInfoBox.unit.ts | 69 +- .../ClassMembersInfoBox.vue | 11 +- src/components/rooms/RoomBaseCard.unit.ts | 35 +- src/components/rooms/RoomBaseCard.vue | 13 +- src/components/rooms/RoomCardChip.unit.ts | 22 +- src/components/rooms/RoomCardChip.vue | 8 +- .../rooms/RoomExternalToolCard.unit.ts | 91 +- src/components/rooms/RoomExternalToolCard.vue | 7 +- .../rooms/RoomVideoConferenceCard.unit.ts | 57 +- .../rooms/RoomVideoConferenceCard.vue | 19 +- src/components/share/ImportFlow.unit.ts | 146 +- src/components/share/ImportFlow.vue | 24 +- src/components/share/ImportModal.unit.ts | 194 +- src/components/share/ImportModal.vue | 24 +- .../share/SelectCourseModal.unit.ts | 55 +- src/components/share/SelectCourseModal.vue | 25 +- src/components/share/ShareModal.unit.ts | 96 +- src/components/share/ShareModal.vue | 28 +- .../share/ShareModalOptionsForm.unit.ts | 46 +- .../share/ShareModalOptionsForm.vue | 49 +- src/components/share/ShareModalResult.unit.ts | 93 +- src/components/share/ShareModalResult.vue | 43 +- .../templates/DefaultWireframe.unit.js | 54 +- src/components/templates/DefaultWireframe.vue | 89 +- .../templates/RoomDashboard.unit.ts | 176 +- src/components/templates/RoomDashboard.vue | 153 +- src/components/templates/RoomWrapper.unit.ts | 98 +- src/components/templates/RoomWrapper.vue | 89 +- .../templates/TasksDashboardMain.unit.ts | 108 +- .../templates/TasksDashboardMain.vue | 172 +- .../templates/TasksDashboardStudent.unit.ts | 55 +- .../templates/TasksDashboardStudent.vue | 16 +- .../templates/TasksDashboardTeacher.unit.ts | 61 +- .../templates/TasksDashboardTeacher.vue | 22 +- .../templates/default-wireframe.types.ts | 2 +- src/components/topbar/HelpDropdown.unit.ts | 13 +- src/components/topbar/HelpDropdown.vue | 86 +- src/components/topbar/LanguageMenu.unit.ts | 73 +- src/components/topbar/LanguageMenu.vue | 52 +- src/components/topbar/MenuQrCode.unit.ts | 71 +- src/components/topbar/MenuQrCode.vue | 8 +- src/components/topbar/PopupIcon.unit.ts | 43 +- src/components/topbar/PopupIcon.vue | 100 +- .../topbar/PopupIconInitials.unit.ts | 34 +- src/components/topbar/PopupIconInitials.vue | 78 +- src/components/topbar/StatusAlerts.unit.ts | 71 +- src/components/topbar/StatusAlerts.vue | 74 +- src/components/topbar/TheTopBar.unit.ts | 131 +- src/components/topbar/TheTopBar.vue | 61 +- src/components/ui-alert/BaseAlert.unit.ts | 2 +- src/components/ui-alert/BaseAlert.vue | 15 +- src/components/ui-board/BoardMenu.unit.ts | 23 +- src/components/ui-board/BoardMenu.vue | 15 +- .../ui-board/BoardMenuAction.unit.ts | 29 +- src/components/ui-board/BoardMenuAction.vue | 23 +- .../ui-board/BoardMenuActionDelete.unit.ts | 38 +- .../ui-board/BoardMenuActionDelete.vue | 17 +- .../ui-board/BoardMenuActionMoveDown.unit.ts | 44 +- .../ui-board/BoardMenuActionMoveLeft.unit.ts | 33 +- .../ui-board/BoardMenuActionMoveRight.unit.ts | 33 +- .../ui-board/BoardMenuActionMoveUp.unit.ts | 47 +- src/components/ui-board/LineClamp.unit.ts | 14 +- src/components/ui-board/LineClamp.vue | 7 +- .../content-element/ContentElementBar.unit.ts | 195 +- .../content-element/ContentElementBar.vue | 159 +- .../ContentElementDisplay.unit.ts | 31 - .../content-element/ContentElementDisplay.vue | 28 - .../ContentElementTitleIcon.unit.ts | 27 - .../ContentElementTitleIcon.vue | 17 - src/components/ui-board/injection-tokens.ts | 5 +- .../ui-color-overlay/ColorOverlay.unit.ts | 382 - .../ui-color-overlay/ColorOverlay.vue | 116 - src/components/ui-color-overlay/index.ts | 3 - .../Confirmation.composable.unit.ts | 4 +- .../ConfirmationDialog.unit.ts | 18 +- .../ConfirmationDialog.vue | 9 +- .../DeleteConfirmation.composable.ts | 16 +- .../DeleteConfirmation.composable.unit.ts | 27 +- .../ui-date-time-picker/DatePicker.unit.ts | 75 +- .../ui-date-time-picker/DatePicker.vue | 255 +- .../DateTimePicker.unit.ts | 91 +- .../ui-date-time-picker/DateTimePicker.vue | 215 +- .../ui-date-time-picker/TimePicker.unit.ts | 86 +- .../ui-date-time-picker/TimePicker.vue | 151 +- .../TimePickerState.composable.ts | 25 - src/components/ui-date-time-picker/index.ts | 4 +- .../ui-light-box/LightBox.composable.unit.ts | 8 +- src/components/ui-light-box/LightBox.unit.ts | 79 +- src/components/ui-light-box/LightBox.vue | 49 +- src/components/ui-light-box/index.ts | 3 +- .../NaturalWidth.composable.ts | 22 - .../NaturalWidth.composable.unit.ts | 63 - .../ui-preview-image/PreviewImage.unit.ts | 64 +- .../ui-preview-image/PreviewImage.vue | 66 +- .../ui-speed-dial-menu/SpeedDialMenu.unit.ts | 166 + .../ui-speed-dial-menu/SpeedDialMenu.vue | 190 + .../SpeedDialMenuAction.vue | 166 + src/components/ui-speed-dial-menu/index.ts | 4 + .../ui-speed-dial-menu/injection-tokens.ts | 11 + .../BoardNotifier.composable.unit.ts | 7 +- .../util-board/extractDataAttribute.util.ts | 18 + .../extractDataAttribute.util.unit.ts | 62 + src/components/util-board/index.ts | 2 + .../util-input-masks/InputMask.factory.ts | 13 +- .../util-input-masks/InputMask.unit.ts | 7 +- .../util-input-masks/InputsMasks.ts | 4 - src/components/util-validators/validators.ts | 32 +- .../util-validators/validators.unit.ts | 18 +- src/components/util-vue/index.ts | 3 + src/components/util-vue/slots.ts | 42 + src/composables/copy.ts | 13 +- src/composables/copy.unit.ts | 17 +- src/composables/i18n.composable.ts | 22 - src/composables/i18n.composable.unit.ts | 77 - src/composables/loadingState.unit.ts | 8 +- src/layouts/legacyLoggedIn.unit.ts | 53 +- src/layouts/legacyLoggedIn.vue | 2 +- src/layouts/lernStore.layout.unit.ts | 79 +- src/layouts/lernStore.layout.vue | 18 +- src/layouts/loggedIn.layout.vue | 29 +- src/layouts/loggedOut.layout.unit.ts | 51 +- src/layouts/loggedOut.layout.vue | 32 +- src/locales/de.json | 1110 - src/locales/de.ts | 1558 + src/locales/en.json | 1110 - src/locales/en.ts | 1527 + src/locales/es.json | 1117 - src/locales/es.ts | 1576 + src/locales/uk.json | 1136 - src/locales/uk.ts | 1550 + src/main.ts | 134 +- src/mixins/controllableData.js | 2 +- src/mixins/infiniteScrolling.js | 2 +- src/mixins/infiniteScrolling.unit.js | 2 +- src/mixins/toastsFromQueryString.unit.js | 13 +- src/mixins/uid.js | 7 - src/mixins/uid.unit.js | 27 - src/pages/ActivationCode.page.vue | 2 +- src/pages/Error.page.vue | 13 +- src/pages/ErrorPage.unit.ts | 70 +- src/pages/Imprint.page.vue | 2 +- src/pages/LernStoreOverview.page.vue | 449 +- src/pages/NewsCreate.page.vue | 15 +- src/pages/NewsEdit.page.vue | 174 +- src/pages/ProxyError.page.vue | 2 +- .../administration/ClassOverview.page.unit.ts | 506 +- .../administration/ClassOverview.page.vue | 147 +- .../administration/LDAPActivate.page.vue | 18 +- src/pages/administration/LDAPConfig.page.vue | 12 +- src/pages/administration/Migration.page.vue | 988 +- .../SchoolSettings.page.unit.ts | 73 +- .../administration/SchoolSettings.page.vue | 320 +- .../administration/StudentConsent.page.vue | 101 +- .../administration/StudentCreate.page.vue | 8 +- .../administration/StudentOverview.page.vue | 100 +- .../administration/TeacherCreate.page.vue | 6 +- .../administration/TeacherOverview.page.vue | 96 +- .../administration/ldap-activate.unit.js | 258 +- src/pages/administration/ldap-config.unit.js | 299 +- src/pages/administration/migration.unit.ts | 357 +- ...choolExternalToolConfigurator.page.unit.ts | 148 +- .../SchoolExternalToolConfigurator.page.vue | 15 +- .../administration/student-consent.unit.js | 239 +- .../administration/student-create.unit.js | 109 +- .../administration/student-overview.unit.js | 385 +- .../administration/teacher-create.unit.js | 163 +- .../administration/teacher-overview.unit.js | 320 +- ...ntextExternalToolConfigurator.page.unit.ts | 93 +- .../ContextExternalToolConfigurator.page.vue | 13 +- src/pages/files/FilesOverview.page.unit.ts | 266 - src/pages/files/FilesOverview.page.vue | 126 - src/pages/files/file-page-config.type.ts | 7 - src/pages/files/file-table-item.ts | 10 - .../files/file-table-utils.composable.ts | 186 - .../files/file-table-utils.composable.unit.ts | 238 - src/pages/h5p/H5PEditor.page.unit.ts | 25 +- src/pages/h5p/H5PEditor.page.vue | 19 +- src/pages/h5p/H5PPlayer.page.unit.ts | 17 +- src/pages/h5p/H5PPlayer.page.vue | 5 +- src/pages/rooms/RoomDetails.page.vue | 173 +- src/pages/rooms/RoomList.page.vue | 38 +- src/pages/rooms/RoomOverview.page.vue | 92 +- src/pages/rooms/room-details.unit.ts | 279 +- src/pages/rooms/room-list.unit.ts | 63 +- src/pages/rooms/room-overview.unit.js | 298 +- .../RoomExternalToolsErrorDialog.unit.ts | 56 +- .../tools/RoomExternalToolsErrorDialog.vue | 12 +- .../tools/RoomExternalToolsOverview.unit.ts | 37 +- .../rooms/tools/RoomExternalToolsOverview.vue | 20 +- .../tools/RoomExternalToolsSection.unit.ts | 98 +- .../rooms/tools/RoomExternalToolsSection.vue | 16 +- .../tools/RoomVideoConferenceSection.unit.ts | 90 +- .../tools/RoomVideoConferenceSection.vue | 100 +- src/pages/tasks/TaskOverview.page.vue | 41 +- src/pages/tasks/TaskOverview.unit.ts | 20 +- .../UserLoginMigrationConsent.page.unit.ts | 42 +- .../UserLoginMigrationConsent.page.vue | 17 +- .../UserLoginMigrationError.page.unit.ts | 49 +- .../UserLoginMigrationError.page.vue | 18 +- .../UserLoginMigrationSuccess.page.unit.ts | 35 +- .../UserLoginMigrationSuccess.page.vue | 15 +- src/plugins/application-error-handler.ts | 4 +- src/plugins/datetime.js | 12 + src/plugins/datetime.unit.js | 19 +- src/plugins/directives.js | 23 - src/plugins/global.d.ts | 4 - src/plugins/i18n-test.d.ts | 8 - src/plugins/i18n-test.js | 34 - src/plugins/i18n.ts | 56 +- src/plugins/i18n.unit.js | 21 +- src/plugins/store.ts | 7 +- src/plugins/vuetify.ts | 20 +- src/router/guards/is-authenticated.guard.ts | 8 +- .../legacy-route-compatibility.guard.ts | 11 +- src/router/guards/multi.guard.ts | 43 +- src/router/guards/multi.guard.unit.ts | 39 +- src/router/guards/permission.guard.ts | 13 +- src/router/guards/permission.guard.unit.ts | 4 +- .../guards/validate-query-parameters.guard.ts | 18 +- .../validate-query-parameters.guard.unit.ts | 4 +- src/router/index.ts | 11 +- src/router/routes.ts | 64 +- src/router/vue-client-route.js | 3 - src/shims-vue.d.ts | 6 +- src/store/auth.ts | 32 - src/store/auth.unit.ts | 28 +- src/store/autoLogout.ts | 12 + src/store/bulkConsent.js | 65 +- src/store/collaborative-files.ts | 167 - src/store/collaborative-files.unit.ts | 231 - .../file-meta-list.response.ts | 8 - .../collaborative-files/file-meta.response.ts | 18 - src/store/content.ts | 7 +- src/store/content.unit.ts | 33 +- src/store/news.ts | 11 +- src/store/news.unit.ts | 12 +- src/store/room.ts | 21 - src/store/room.unit.ts | 96 - src/store/rooms.ts | 27 - src/store/rooms.unit.ts | 18 - src/store/store-accessor.ts | 4 - src/store/types/alert-payload.ts | 4 +- src/store/types/collaborative-file.ts | 42 - src/store/types/content.ts | 70 +- src/store/types/data-table-header.ts | 10 + src/store/types/env-config.ts | 1 - src/store/types/news.ts | 16 +- src/store/types/schools.ts | 2 +- src/styles/global.scss | 4 +- src/styles/settings.scss | 58 + src/styles/utility/_body.scss | 2 +- src/styles/utility/_buttons.scss | 11 + src/styles/utility/_cards.scss | 3 + src/styles/utility/_colors.scss | 6 +- src/styles/utility/_links.scss | 3 + src/styles/utility/_resets.scss | 34 - src/styles/utility/_typography.scss | 4 +- src/styles/variables.scss | 2 +- src/styles/vuetify-settings.scss | 3 + src/themes/base-vuetify.options.js | 70 +- .../brb/components/legacy/TheFooter.vue | 8 +- src/themes/brb/vuetify.options.js | 20 +- .../n21/components/legacy/TheFooter.vue | 8 +- src/themes/n21/vuetify.options.js | 20 +- .../thr/components/legacy/TheFooter.vue | 24 +- src/themes/thr/vuetify.options.js | 20 +- src/types/board/DragAndDrop.ts | 16 +- src/types/vuetify/index.ts | 21 + src/utils/TestUtil.js | 14 - src/utils/adminFilter.js | 337 - src/utils/adminFilter.unit.js | 265 - src/utils/api.ts | 7 +- src/utils/api.unit.ts | 11 +- src/utils/inject/inject-strict.unit.ts | 8 +- src/utils/inject/injection-keys.ts | 9 +- src/utils/ldapConstants.js | 5 +- src/utils/service-template.js | 16 +- src/utils/sidebar-base-items.ts | 9 - src/utils/uid.ts | 15 + src/vue-i18n.d.ts | 13 + src/vuetify.options.js | 20 +- tests/general.unit.js | 7 - tests/setup.js | 28 + tests/test-utils/componentMocks.ts | 52 - .../factory/boardResponseFactory.ts | 2 +- .../test-utils/factory/cardResponseFactory.ts | 2 +- .../factory/columnResponseFactory.ts | 2 +- .../drawingContentElementResponseFactory.ts | 2 +- .../factory/fileElementResponseFactory.ts | 2 +- tests/test-utils/factory/index.ts | 1 + .../factory/linkElementResponseFactory.ts | 4 +- tests/test-utils/factory/schoolFactory.ts | 7 + ...bmissionContainerElementResponseFactory.ts | 2 +- .../factory/submissionItemResponseFactory.ts | 2 +- .../factory/submissionsResponseFactory.ts | 6 +- tests/test-utils/i18nMock.ts | 36 +- tests/test-utils/index.ts | 1 - .../{mockDataTasks.js => mockDataTasks.ts} | 45 +- tests/test-utils/mountComposable.ts | 25 +- tests/test-utils/setup/createTestingI18n.ts | 20 + .../test-utils/setup/createTestingVuetify.ts | 31 + tests/test-utils/setup/index.ts | 2 + tests/test-utils/setupStores.js | 4 +- tests/unit/setup.js | 247 - tsconfig.json | 9 +- vue.config.js | 22 +- webpack-config/vue-i18n-loader.js | 39 + 702 files changed, 29340 insertions(+), 49335 deletions(-) create mode 100644 VUE3-UPGRADE-RESULTS/BREAKING-CHANGES.md create mode 100644 VUE3-UPGRADE-RESULTS/BUILD.md create mode 100644 VUE3-UPGRADE-RESULTS/DATA-TABLES.md create mode 100644 VUE3-UPGRADE-RESULTS/DEPENDENCY-UPGRADES.md create mode 100644 VUE3-UPGRADE-RESULTS/ESTIMATION.md create mode 100644 VUE3-UPGRADE-RESULTS/FilesToBeRefactored.md create mode 100644 VUE3-UPGRADE-RESULTS/STORE-REFACTORING.md create mode 100644 VUE3-UPGRADE-RESULTS/TESTING.md create mode 100644 VUE3-UPGRADE-RESULTS/VUE3-UPGRADE.md create mode 100644 VUE3-UPGRADE-RESULTS/VUETIFY-UPGRADE.md delete mode 100644 jsconfig.json delete mode 100644 src/components/atoms/InfoMessage.unit.js create mode 100644 src/components/atoms/InfoMessage.unit.ts delete mode 100644 src/components/atoms/vCustomAutocomplete.vue delete mode 100644 src/components/atoms/vCustomChipTimeRemaining.unit.js create mode 100644 src/components/atoms/vCustomChipTimeRemaining.unit.ts delete mode 100644 src/components/atoms/vCustomFab.unit.ts delete mode 100644 src/components/atoms/vCustomFab.vue delete mode 100644 src/components/atoms/vCustomSwitch.unit.ts delete mode 100644 src/components/atoms/vCustomSwitch.vue delete mode 100644 src/components/base/BaseInput/BaseInputRadio.unit.js delete mode 100644 src/components/base/_globals.js create mode 100644 src/components/base/components.js rename src/components/{molecules => feature-news-form}/FormActions.unit.js (100%) rename src/components/{molecules => feature-news-form}/FormActions.vue (100%) rename src/components/{organisms => feature-news-form}/FormNews.unit.ts (64%) rename src/components/{organisms => feature-news-form}/FormNews.vue (91%) rename src/components/{molecules => feature-news-form}/TitleInput.unit.js (100%) create mode 100644 src/components/feature-news-form/TitleInput.vue create mode 100644 src/components/feature-news-form/index.ts delete mode 100644 src/components/legacy/TheSidebar.unit.js create mode 100644 src/components/legacy/TheSidebar.unit.ts rename src/components/{organisms => lern-store}/AddContentButton.vue (87%) rename src/components/{molecules/AddContentModal.unit.js => lern-store/AddContentModal.unit.ts} (60%) rename src/components/{molecules => lern-store}/AddContentModal.vue (80%) create mode 100644 src/components/lern-store/ContentCard.unit.js rename src/components/{organisms => lern-store}/ContentCard.vue (89%) create mode 100644 src/components/lern-store/ContentEduSharingFooter.unit.ts rename src/components/{molecules => lern-store}/ContentEduSharingFooter.vue (100%) create mode 100644 src/components/lern-store/ContentEmptyState.unit.ts rename src/components/{molecules => lern-store}/ContentEmptyState.vue (70%) create mode 100644 src/components/lern-store/ContentInitialState.unit.ts rename src/components/{molecules => lern-store}/ContentInitialState.vue (63%) create mode 100644 src/components/lern-store/LoadingModal.unit.ts rename src/components/{molecules => lern-store}/LoadingModal.vue (69%) rename src/components/{molecules/NotificationModal.unit.js => lern-store/NotificationModal.unit.ts} (63%) rename src/components/{molecules => lern-store}/NotificationModal.vue (85%) delete mode 100644 src/components/molecules/ContentEduSharingFooter.unit.js delete mode 100644 src/components/molecules/ContentEmptyState.unit.js delete mode 100644 src/components/molecules/ContentInitialState.unit.js delete mode 100644 src/components/molecules/ContentSearchbar.unit.js delete mode 100644 src/components/molecules/ContentSearchbar.vue rename src/components/molecules/{ContextMenu.unit.js => ContextMenu.unit.ts} (71%) delete mode 100644 src/components/molecules/EmptyState.unit.js delete mode 100644 src/components/molecules/EmptyState.vue delete mode 100644 src/components/molecules/ImportModal.unit.ts delete mode 100644 src/components/molecules/ImportModal.vue delete mode 100644 src/components/molecules/InfoBox.unit.js create mode 100644 src/components/molecules/InfoBox.unit.ts delete mode 100644 src/components/molecules/LoadingModal.unit.js rename src/components/molecules/{ProgressModal.unit.js => ProgressModal.unit.ts} (62%) delete mode 100644 src/components/molecules/SkipLinks.unit.js create mode 100644 src/components/molecules/SkipLinks.unit.ts rename src/components/molecules/{TaskItemMenu.unit.js => TaskItemMenu.unit.ts} (77%) rename src/components/molecules/{TaskItemStudent.unit.js => TaskItemStudent.unit.ts} (74%) rename src/components/molecules/{TaskItemTeacher.unit.js => TaskItemTeacher.unit.ts} (74%) delete mode 100644 src/components/molecules/TitleInput.vue rename src/components/molecules/{vCustomDoublePanels.unit.js => vCustomDoublePanels.unit.ts} (66%) rename src/components/molecules/{vCustomEmptyState.unit.js => vCustomEmptyState.unit.ts} (57%) delete mode 100644 src/components/organisms/ContentCard.unit.js delete mode 100644 src/components/organisms/DataFilter/DataFilter.md create mode 100644 src/components/organisms/DataFilter/DataFilter.unit.ts delete mode 100644 src/components/organisms/DataFilter/DataFilterChips.vue delete mode 100644 src/components/organisms/DataFilter/DataFilterLayout.unit.js delete mode 100644 src/components/organisms/DataFilter/DataFilterLayout.vue delete mode 100644 src/components/organisms/DataFilter/DataFilterModal.vue delete mode 100644 src/components/organisms/DataFilter/DataFilterSelect.vue create mode 100644 src/components/organisms/DataFilter/FilterDialog.unit.ts create mode 100644 src/components/organisms/DataFilter/FilterDialog.vue create mode 100644 src/components/organisms/DataFilter/composables/filter.composable.ts create mode 100644 src/components/organisms/DataFilter/composables/filter.composable.unit.ts create mode 100644 src/components/organisms/DataFilter/composables/localStorage.composable.ts create mode 100644 src/components/organisms/DataFilter/composables/localStorage.composable.unit.ts delete mode 100644 src/components/organisms/DataFilter/defaultFilters.js delete mode 100644 src/components/organisms/DataFilter/defaultFilters.unit.js create mode 100644 src/components/organisms/DataFilter/filterComponents/DateBetween.unit.ts create mode 100644 src/components/organisms/DataFilter/filterComponents/DateBetween.vue create mode 100644 src/components/organisms/DataFilter/filterComponents/FilterActionButton.unit.ts create mode 100644 src/components/organisms/DataFilter/filterComponents/FilterActionButtons.vue create mode 100644 src/components/organisms/DataFilter/filterComponents/FilterChips.unit.ts create mode 100644 src/components/organisms/DataFilter/filterComponents/FilterChips.vue create mode 100644 src/components/organisms/DataFilter/filterComponents/ListSelection.unit.ts create mode 100644 src/components/organisms/DataFilter/filterComponents/ListSelection.vue create mode 100644 src/components/organisms/DataFilter/filterComponents/index.ts delete mode 100644 src/components/organisms/DataFilter/inputs/Checkbox.unit.js delete mode 100644 src/components/organisms/DataFilter/inputs/Checkbox.vue delete mode 100644 src/components/organisms/DataFilter/inputs/Default.unit.js delete mode 100644 src/components/organisms/DataFilter/inputs/Default.vue delete mode 100644 src/components/organisms/DataFilter/inputs/Inputs.md delete mode 100644 src/components/organisms/DataFilter/inputs/Radio.unit.js delete mode 100644 src/components/organisms/DataFilter/inputs/Radio.vue create mode 100644 src/components/organisms/DataFilter/types/enums.ts create mode 100644 src/components/organisms/DataFilter/types/index.ts create mode 100644 src/components/organisms/DataFilter/types/types.ts delete mode 100644 src/components/organisms/DataTable/DataTable.md delete mode 100644 src/components/organisms/DataTable/DataTable.unit.js delete mode 100644 src/components/organisms/DataTable/DataTable.vue delete mode 100644 src/components/organisms/vCustomDialog.unit.js create mode 100644 src/components/organisms/vCustomDialog.unit.ts delete mode 100644 src/components/ui-board/content-element/ContentElementDisplay.unit.ts delete mode 100644 src/components/ui-board/content-element/ContentElementDisplay.vue delete mode 100644 src/components/ui-board/content-element/ContentElementTitleIcon.unit.ts delete mode 100644 src/components/ui-board/content-element/ContentElementTitleIcon.vue delete mode 100644 src/components/ui-color-overlay/ColorOverlay.unit.ts delete mode 100644 src/components/ui-color-overlay/ColorOverlay.vue delete mode 100644 src/components/ui-color-overlay/index.ts delete mode 100644 src/components/ui-date-time-picker/TimePickerState.composable.ts delete mode 100644 src/components/ui-preview-image/NaturalWidth.composable.ts delete mode 100644 src/components/ui-preview-image/NaturalWidth.composable.unit.ts create mode 100644 src/components/ui-speed-dial-menu/SpeedDialMenu.unit.ts create mode 100644 src/components/ui-speed-dial-menu/SpeedDialMenu.vue create mode 100644 src/components/ui-speed-dial-menu/SpeedDialMenuAction.vue create mode 100644 src/components/ui-speed-dial-menu/index.ts create mode 100644 src/components/ui-speed-dial-menu/injection-tokens.ts create mode 100644 src/components/util-board/extractDataAttribute.util.ts create mode 100644 src/components/util-board/extractDataAttribute.util.unit.ts create mode 100644 src/components/util-vue/index.ts create mode 100644 src/components/util-vue/slots.ts delete mode 100644 src/composables/i18n.composable.ts delete mode 100644 src/composables/i18n.composable.unit.ts delete mode 100644 src/locales/de.json create mode 100644 src/locales/de.ts delete mode 100644 src/locales/en.json create mode 100644 src/locales/en.ts delete mode 100644 src/locales/es.json create mode 100644 src/locales/es.ts delete mode 100644 src/locales/uk.json create mode 100644 src/locales/uk.ts delete mode 100644 src/mixins/uid.js delete mode 100644 src/mixins/uid.unit.js delete mode 100644 src/pages/files/FilesOverview.page.unit.ts delete mode 100644 src/pages/files/FilesOverview.page.vue delete mode 100644 src/pages/files/file-page-config.type.ts delete mode 100644 src/pages/files/file-table-item.ts delete mode 100644 src/pages/files/file-table-utils.composable.ts delete mode 100644 src/pages/files/file-table-utils.composable.unit.ts delete mode 100644 src/plugins/directives.js delete mode 100644 src/plugins/i18n-test.d.ts delete mode 100644 src/plugins/i18n-test.js delete mode 100644 src/store/collaborative-files.ts delete mode 100644 src/store/collaborative-files.unit.ts delete mode 100644 src/store/collaborative-files/file-meta-list.response.ts delete mode 100644 src/store/collaborative-files/file-meta.response.ts delete mode 100644 src/store/types/collaborative-file.ts create mode 100644 src/store/types/data-table-header.ts create mode 100644 src/styles/settings.scss create mode 100644 src/styles/utility/_buttons.scss create mode 100644 src/styles/utility/_cards.scss create mode 100644 src/styles/utility/_links.scss delete mode 100644 src/styles/utility/_resets.scss create mode 100644 src/styles/vuetify-settings.scss create mode 100644 src/types/vuetify/index.ts delete mode 100644 src/utils/TestUtil.js delete mode 100644 src/utils/adminFilter.js delete mode 100644 src/utils/adminFilter.unit.js create mode 100644 src/utils/uid.ts create mode 100644 src/vue-i18n.d.ts delete mode 100644 tests/general.unit.js create mode 100644 tests/setup.js delete mode 100644 tests/test-utils/componentMocks.ts create mode 100644 tests/test-utils/factory/schoolFactory.ts rename tests/test-utils/{mockDataTasks.js => mockDataTasks.ts} (90%) create mode 100644 tests/test-utils/setup/createTestingI18n.ts create mode 100644 tests/test-utils/setup/createTestingVuetify.ts create mode 100644 tests/test-utils/setup/index.ts delete mode 100644 tests/unit/setup.js create mode 100644 webpack-config/vue-i18n-loader.js diff --git a/.eslintrc.js b/.eslintrc.js index c1d6c207f1..4666bf38b2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,7 +4,7 @@ module.exports = { node: true, }, extends: [ - "plugin:vue/essential", + "plugin:vue/vue3-essential", "eslint:recommended", "@vue/typescript/recommended", "plugin:prettier/recommended", @@ -13,9 +13,9 @@ module.exports = { ecmaVersion: 2020, }, rules: { + "@typescript-eslint/no-explicit-any": "warn", "no-console": process.env.NODE_ENV === "production" ? "off" : "warn", "no-debugger": process.env.NODE_ENV === "production" ? "off" : "warn", - // ---- "no-useless-escape": "error", "no-irregular-whitespace": "error", "no-undef": "warn", @@ -28,7 +28,6 @@ module.exports = { "@typescript-eslint/ban-ts-comment": "error", "@typescript-eslint/no-inferrable-types": "error", "@typescript-eslint/ban-types": "error", - "@typescript-eslint/no-explicit-any": "warn", "vue/no-v-text-v-html-on-component": "error", "vue/no-v-html": "error", "vue/html-self-closing": [ @@ -119,9 +118,6 @@ module.exports = { globals: { mount: false, shallowMount: false, - createComponentMocks: false, - rendersSlotContent: false, - wait: false, }, }, ], diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 2710cc2b1e..b64438899d 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,4 +1,4 @@ -name: 'Dependency Review' +name: "Dependency Review" on: [pull_request] permissions: @@ -9,9 +9,9 @@ jobs: dependency-review: runs-on: ubuntu-latest steps: - - name: 'Checkout Repository' + - name: "Checkout Repository" uses: actions/checkout@v4 - - name: 'Dependency Review' + - name: "Dependency Review" uses: actions/dependency-review-action@v4 with: - allow-licenses: AGPL-3.0-only, LGPL-3.0, MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, X11, 0BSD, GPL-3.0, AGPL-3.0 + allow-licenses: AGPL-3.0-only, LGPL-3.0, MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, X11, 0BSD, GPL-3.0, AGPL-3.0, CC-BY-4.0 diff --git a/.prettierignore b/.prettierignore index 6419b3622b..0c75e54a8a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,3 +6,4 @@ LICENSE.md /src/serverApi/** /src/fileStorageApi/** /src/h5pEditorApi/** +/VUE3-UPGRADE-RESULTS/**/*.md diff --git a/VUE3-UPGRADE-RESULTS/BREAKING-CHANGES.md b/VUE3-UPGRADE-RESULTS/BREAKING-CHANGES.md new file mode 100644 index 0000000000..f4c479467a --- /dev/null +++ b/VUE3-UPGRADE-RESULTS/BREAKING-CHANGES.md @@ -0,0 +1,21 @@ +# BREAKING CHANGES + +- [Vue.2x -> Vue.3x](https://v3-migration.vuejs.org/breaking-changes/) +- [Vuetify.2x -> Vuetify.3x](https://vuetifyjs.com/en/getting-started/upgrade-guide/) +- [Vuex.3x -> Vuex.4x](https://vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html) +- [Vue Router.3x -> Vuex.4x](https://router.vuejs.org/guide/migration/) +- [Vue Test Utils](https://test-utils.vuejs.org/migration/) +- [vue instance vs app instance](https://v3-migration.vuejs.org/breaking-changes/global-api.html) + + | 2.x Global API | 3.x Instance API (app) | + | -------------------------- | ------------------------------------------ | + | Vue.config | app.config | + | Vue.config.productionTip | **removed** | + | Vue.config.ignoredElements | app.config.compilerOptions.isCustomElement | + | Vue.component | app.component | + | Vue.directive | app.directive | + | Vue.mixin | app.mixin | + | Vue.use | app.use | + | Vue.prototype | app.config.globalProperties | + | Vue.extend | **removed** | + | Vue.set | can be replaced by just setting `reactive` properties (used in `src/utils/service-template.js`) | diff --git a/VUE3-UPGRADE-RESULTS/BUILD.md b/VUE3-UPGRADE-RESULTS/BUILD.md new file mode 100644 index 0000000000..766fa757a8 --- /dev/null +++ b/VUE3-UPGRADE-RESULTS/BUILD.md @@ -0,0 +1,17 @@ +# Build + +## Vue-I18n + +We have to precompile our i18n translations. That can be done using the `@intlify/bundle-tools` package: + +https://github.com/intlify/bundle-tools + +For webpack that means we have to use the `@intlify/vue-i18n-loader` package. This reports lots of errors when building because we have HTML-tags in our locales (which is generally not allowed from a XSS security perspective). + +There is an option `strictMessage: false` for the locales compilation but this option is not supported in `@intlify/vue-i18n-loader`. + +Possible solutions: + +1. Implement an own (quiet simple) custom resolver for locales compilation in webpack, see https://github.com/intlify/bundle-tools/blob/main/packages/vue-i18n-loader/src/index.ts + +2. Move to unplugin/webpack and use the newer package `@intlify/unplugin-vue-i18n`. However this can be a lot of effort depending on how much we will have to change in our build process. \ No newline at end of file diff --git a/VUE3-UPGRADE-RESULTS/DATA-TABLES.md b/VUE3-UPGRADE-RESULTS/DATA-TABLES.md new file mode 100644 index 0000000000..fc0c81d8da --- /dev/null +++ b/VUE3-UPGRADE-RESULTS/DATA-TABLES.md @@ -0,0 +1,9 @@ +# DATA TABLES + +- Currently, we have `DataTable` and `BackendDataTable` components. Their structures are very complicated and hard to maintain. We recommend refactoring the data tables with Vuetify components. So that we have clean, maintainable code. After changing, we also have a chance to remove all `base` components. + + +- We have 3 options: + 1. Reimplement `vue-filter-ui` dependency as our own + 2. Refactor `DataFilter` component + 3. Refactor `DataTables` component diff --git a/VUE3-UPGRADE-RESULTS/DEPENDENCY-UPGRADES.md b/VUE3-UPGRADE-RESULTS/DEPENDENCY-UPGRADES.md new file mode 100644 index 0000000000..642bc572c5 --- /dev/null +++ b/VUE3-UPGRADE-RESULTS/DEPENDENCY-UPGRADES.md @@ -0,0 +1,71 @@ +# DEPENDENCIES + +## Upgraded + +- "@ckeditor/ckeditor5-vue": "^5.1.0" +- "@mdi/js": "^7.2.96" (devDep) +- "@vuelidate/core": "^2.0.3" +- "@vuelidate/validators": "^2.0.4" +- "vue": "^3.3.4" +- "vue-dndrop": "^1.3.1" ?? +- "vue-i18n": "^9.2.2" +- "vue-router": "^4.2.4" +- "vuedraggable": "^4.1.0" +- "vuetify": "^3.3.14" +- "vuex": "^4.0.2" + +## Removed + +- "flush-promises": "^1.0.2" +- "tiptap": "^1.32.2" +- "tiptap-extensions": "^1.35.2" +- "vue-mq": "^1.0.1" +- "vuelidate": "^0.7.7" + +## Added + +- "vue3-mq": "^3.1.3" +- "resize-observer-polyfill": "^1.5.1" + +## Can not be upgraded + +- "vue-filter-ui": "^0.8.0" + +## Library Notes + +### vue-i18n + +```html + + + +``` + +### vue-dndrop + +```sh +npm i vue-dndrop@next +``` + +_Note: should be replaced by `vue-draggable` (based on sortable.js) because of re-rendering issues_ + +### Vue3-MQ + +`$mq` has to be replaced by injection + + + +### vue-filter-ui + +- The `DataTable` component uses `vue-filter-ui` dependency which is not competible with vue-3. And seems there is no info or plan to upgrade the dependeny in its repository. +- **Recommended Solution-1:** Implement our own methods as the dependency does. +- **Recommended Solution-2:** Refactor `datatables` with vuetify components. diff --git a/VUE3-UPGRADE-RESULTS/ESTIMATION.md b/VUE3-UPGRADE-RESULTS/ESTIMATION.md new file mode 100644 index 0000000000..9f1f199eac --- /dev/null +++ b/VUE3-UPGRADE-RESULTS/ESTIMATION.md @@ -0,0 +1,17 @@ +# ESTIMATIONS FOR VUE.3x UPGRADE + +| Issue | Remarks | Effort Estimation min PD | Effort Estimation max PD | Comment | +| ---------------------------- | ------------------------------------------------------------ | -----------------------: | -----------------------: | -------------------- | +| Dependency Upgrades | [details](DEPENDENCY-UPGRADES.md) | 3 | 5 | | +| Breaking Changes | [details](BREAKING-CHANGES.md) | - | - | covered by following | +| Vue 3.x Upgrade | [details](VUE3-UPGRADE.md) | 3 | 5 | | +| Vuetify 3.x Upgrade | [details](VUETIFY-UPGRADE.md) | 7 | 10 | | +| Refactoring Pages/Components | [details](FilesToBeRefactored.md) | 15 | 20 | | +| Refactoring Unit Tests | [details](TESTING.md) | 20 | 25 | | +| Refactoring Stores | [details](STORE-REFACTORING.md) | 1 | 2 | | +| Refactoring DataTables | [details](DATA-TABLES.md) | 5 | 7 | decided for Option-1 | +| | - Option-1 Reimplement `vue-filter-ui` dependency as our own | 5 | 7 | | +| | - Option-2 Refactor `DataFilter` | - | - | | +| | - Option-3 Refactor `DataTables` | - | - | | +| Unpredictable issues | | 3 | 5 | | +| **TOTAL** | | 57 | 79 | | diff --git a/VUE3-UPGRADE-RESULTS/FilesToBeRefactored.md b/VUE3-UPGRADE-RESULTS/FilesToBeRefactored.md new file mode 100644 index 0000000000..038fda6bdc --- /dev/null +++ b/VUE3-UPGRADE-RESULTS/FilesToBeRefactored.md @@ -0,0 +1,85 @@ +# REFACTORING LIST AND ESTIMATIONS + +## FILES MUST BE REFACTORED + +### `src/components/organisms/DataFilter/DataFilter.vue` + +- The component uses `vue-filter-ui` dependency which is not competible with vue-3. And seems there is no info or plan to upgrade the dependeny in its repository. +- **Recommended Solution:** We need to find a way to get rid of the dependency. +- **Potantial Effort:** The custom data tables are using this component, so the data tables must be refactored. +- **Estimation time for refactoring:** _TBD_ + +### `src/utils/service-template.js` + +- This util file is used for generalizing the boilerplate of using basis of some vuex stores. And it uses `Vue.set()` function which is deprecated in vue-3. +- **Recommended Solution-1:** Find another way to introduce some data into the vue instance as `Vue.set()` does in the vue-2 version. +- **Recommended Solution-2:** Refactor the store files which are currently using the `service-template.js` file. The store files are `activation.js, calendar.js, classes.js, content-search.js, course-group.js, courses.js, lessons.js, public-teachers.js, teams.js, users.js` +- **Potantial Effort:** depending on selected solution +- **Estimation time for refactoring:** _TBD depending on selected solution_ + +### `src/pages/rooms/RoomOverview.page.vue` + +- The moving avatar components are based on the `this.$refs` selector. The `$refs` object's properties have been changed in vue-3. So we need to find another way to spot the dragging object properties instead of using `$refs`. This whole page refactoring might be necessary. +- **Recommended Solution:** Find another way to spot the dragging component properties. Especially `getElementNameByRef` method should be replaced. +- **Estimation time for refactoring:** _TBD_ + +### [v-model breaking change](https://v3-migration.vuejs.org/breaking-changes/v-model.html) + +- Some components use `model property` inside which is changed in vue-3 and they need some tiny refactorings. +- These components: + - `src/components/atoms/vCustomAutocomplete.vue` -- completely removed, vuetify autocomplete component is used instead + - `src/components/atoms/vCustomSwitch.vue` -- removed, vuetify v-switch will be used + - `src/components/base/BaseInput/BaseInput.vue` + - `src/components/base/BaseInput/BaseInputCheckbox.vue` + - `src/components/base/BaseInput/BaseInputDefault.vue` + - `src/components/base/BaseInput/BaseInputHidden.vue` + - `src/components/base/BaseInput/BaseInputRadio.vue` + - `src/components/molecules/ImportModal.vue` + - `src/components/molecules/RoomModal.vue` + - `src/components/molecules/TextEditor.vue` + - `src/components/molecules/TitleInput.vue` + - `src/components/organisms/FormNews.vue` + - `src/components/organisms/Pagination.vue` + + `src/components/organisms/vCustomDialog.vue` +- **Estimation time for refactoring:** _TBD_ + +## VUETIFY BASED REFACTORINGS + +### Vuetify Labs Component + +- Some vuetify-3 components are not released yet. +- These components: + - `v-calendar` + - `v-date-picker` + - `v-data-table` + - `v-skeleton-loader` + - `v-stepper` + - `v-time-picker` + - `v-tree-view` + - `v-data-iterator` +- **Recommended Solution-1:** Wait until it's upcoming release. +- **Recommended Solution-2:** Imported from vuelabs to use until they're released. But it needs another refactoring after then. +- **Estimation time for refactoring:** _TBD depending on selected solution_ + +### Some vuetify components props' usage have been changed. The detailed list can be found [here](VUE3-UPGRADE-SUMMARY.md) + +- These components: + - `v-menu` + - `v-list` + - `v-list-item` + - `v-alert` + - `v-btn` + - `v-input` + - `v-checkbox` + - `v-radio` + - `v-switch` + - `v-tabs` + - `v-menu` + - `v-select` + - `v-combobox` + - `v-autocomplete` + - `v-expansion-panel` + - `v-card` + - `v-dialog` +- They need tiny refactoring with changing renaming their props etc. +- **Estimation time for refactoring:** _TBD_ diff --git a/VUE3-UPGRADE-RESULTS/STORE-REFACTORING.md b/VUE3-UPGRADE-RESULTS/STORE-REFACTORING.md new file mode 100644 index 0000000000..1e312147d6 --- /dev/null +++ b/VUE3-UPGRADE-RESULTS/STORE-REFACTORING.md @@ -0,0 +1,21 @@ +# STORE REFACTORING + +- `src/utils/service-template.js` +- This util file is used for generalizing the boilerplate of using basis of some vuex stores. And it uses `Vue.set()` function which is deprecated in vue-3. + +- **Recommended Solution:** Refactor the store files which are currently using the `service-template.js` file. Introducing **Pinia** store will be good for these refactorings. + +- The store files are: + +```sh +activation.js +calendar.js +classes.js +content-search.js +course-group.js +courses.js, +lessons.js +public-teachers.js +teams.js, +users.js +``` diff --git a/VUE3-UPGRADE-RESULTS/TESTING.md b/VUE3-UPGRADE-RESULTS/TESTING.md new file mode 100644 index 0000000000..c3ce14fd82 --- /dev/null +++ b/VUE3-UPGRADE-RESULTS/TESTING.md @@ -0,0 +1,61 @@ +# UNIT TESTS + +## Testing + +### Setup +We have a couple of test helpers taht can be used to setup unit tests: `tests/test-utils/setup/index.ts` + +The global setup file `tests/setup.js` has to be refactored and adapted to the new requirements. +### Mounting a component + +Example: `src/components/organisms/vCustomDialog.unit.ts` + +We added helpers for creating the nessesary vue plugins, currently `vuetify` and `i18n`. This is necessary because the setup of the plugins can differ heavily from other environments. In the case of vuetify e.g. the way we have to import vuetify in a Jest test setup has changed. + +```typescript +const wrapper = mount(Component, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + // Note: "propsData" is deprecated now. we should use "props" instead + props: {} +}); +``` + +The helper files can be found in `tests/test-utils/setup/` folder. + +### Testing dialogs + +Vuetify is using the `` component now to "teleport" the contents of the dialog outside the components own ` - diff --git a/src/components/atoms/vCustomChipTimeRemaining.unit.js b/src/components/atoms/vCustomChipTimeRemaining.unit.js deleted file mode 100644 index 2fc7bfe8a3..0000000000 --- a/src/components/atoms/vCustomChipTimeRemaining.unit.js +++ /dev/null @@ -1,117 +0,0 @@ -import Vuetify from "vuetify"; -import VCustomChipTimeRemaining from "./VCustomChipTimeRemaining"; - -let vuetify; - -describe("@/components/atoms/vCustomChipTimeRemaining", () => { - beforeEach(() => { - vuetify = new Vuetify(); - }); - - it("should render an orange v-chip component, with n hours left", () => { - const dueDate = new Date(); - const addHour = 3; - dueDate.setHours(dueDate.getHours() + addHour); - - const wrapper = mount(VCustomChipTimeRemaining, { - ...createComponentMocks({ - i18n: true, - vuetify: true, - }), - vuetify, - propsData: { - type: "warning", - dueDate: dueDate.toISOString(), - }, - }); - - const expectedResult = `${wrapper.vm.$t( - "components.atoms.VCustomChipTimeRemaining.hintDueTime" - )}${addHour - 1} ${wrapper.vm.$tc( - "components.atoms.VCustomChipTimeRemaining.hintHours", - addHour - 1 - )}`; - expect(wrapper.element.textContent).toContain(expectedResult); - }); - - it("should render an orange v-chip component, with n minutes left", () => { - const dueDate = new Date(); - const addMinute = 20; - dueDate.setMinutes(dueDate.getMinutes() + addMinute); - - const wrapper = mount(VCustomChipTimeRemaining, { - ...createComponentMocks({ - i18n: true, - vuetify: true, - }), - vuetify, - propsData: { - type: "warning", - dueDate: dueDate.toISOString(), - }, - }); - - const expectedResult = `${wrapper.vm.$t( - "components.atoms.VCustomChipTimeRemaining.hintDueTime" - )}${addMinute - 1} ${wrapper.vm.$tc( - "components.atoms.VCustomChipTimeRemaining.hintMinutes", - addMinute - 1 - )}`; - - expect(wrapper.element.textContent).toContain(expectedResult); - }); - - it("hintDueDate() method return the right label dependent on date", () => { - let label; - const dueDate = new Date(); - dueDate.setMinutes(dueDate.getMinutes() + 20); - - const wrapper = shallowMount(VCustomChipTimeRemaining, { - ...createComponentMocks({ - i18n: true, - vuetify: true, - }), - vuetify, - propsData: { - type: "warning", - dueDate: dueDate.toISOString(), - }, - }); - - label = wrapper.vm.hintDueDate(dueDate.toISOString()); - expect(label).toContain("19 Minuten"); - label = wrapper.vm.hintDueDate(dueDate.toISOString(), true); - expect(label).toContain("19 min"); - - dueDate.setHours(dueDate.getHours() + 2); - label = wrapper.vm.hintDueDate(dueDate.toISOString()); - expect(label).toContain("2 Stunden"); - label = wrapper.vm.hintDueDate(dueDate.toISOString(), true); - expect(label).toContain("2 h"); - }); - - it("accepts valid type props", () => { - const validTypes = ["warning"]; - const { validator } = VCustomChipTimeRemaining.props.type; - - validTypes.forEach((type) => { - expect(validator(type)).toBe(true); - }); - - expect(validator("wrong type")).toBe(false); - }); - - it("accepts valid dueDate props", () => { - const validDueDates = [ - "2021-06-11T14:00:00.000Z", - "2021-06-07T09:30:00.000Z", - ]; - const { validator } = VCustomChipTimeRemaining.props.dueDate; - - validDueDates.forEach((dueDate) => { - expect(validator(dueDate)).toBe(true); - }); - - expect(validator("wrong due date")).toBe(false); - }); -}); diff --git a/src/components/atoms/vCustomChipTimeRemaining.unit.ts b/src/components/atoms/vCustomChipTimeRemaining.unit.ts new file mode 100644 index 0000000000..1d79591b6e --- /dev/null +++ b/src/components/atoms/vCustomChipTimeRemaining.unit.ts @@ -0,0 +1,116 @@ +import VCustomChipTimeRemaining from "./VCustomChipTimeRemaining.vue"; +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; +import { mount } from "@vue/test-utils"; + +describe("@/components/atoms/vCustomChipTimeRemaining", () => { + const setup = (dueDate: Date, shortenUnit = false) => { + const wrapper = mount(VCustomChipTimeRemaining, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + props: { + type: "warning", + dueDate: dueDate.toISOString(), + shortenUnit, + }, + }); + + return { wrapper }; + }; + + describe("time remaining hint hours", () => { + let dueDate: Date; + const HOURS_UNTIL_DUE = 3; + + beforeEach(() => { + dueDate = new Date(); + dueDate.setHours(dueDate.getHours() + HOURS_UNTIL_DUE); + }); + + it("renders in long form", () => { + const { wrapper } = setup(dueDate); + + const expectedResult = `${wrapper.vm.$t( + "components.atoms.VCustomChipTimeRemaining.hintDueTime" + )}${HOURS_UNTIL_DUE - 1} ${wrapper.vm.$t( + "components.atoms.VCustomChipTimeRemaining.hintHours", + HOURS_UNTIL_DUE - 1 + )}`; + expect(wrapper.element.textContent).toContain(expectedResult); + }); + + it("should render in shortened form", () => { + const { wrapper } = setup(dueDate, true); + + const expectedResult = `${wrapper.vm.$t( + "components.atoms.VCustomChipTimeRemaining.hintDueTime" + )}${HOURS_UNTIL_DUE - 1} ${wrapper.vm.$t( + "components.atoms.VCustomChipTimeRemaining.hintHoursShort" + )}`; + expect(wrapper.element.textContent).toContain(expectedResult); + }); + }); + + describe("time remaining hint minutes", () => { + let dueDate: Date; + const MINUTES_UNTIL_DUE = 20; + + beforeEach(() => { + dueDate = new Date(); + dueDate.setMinutes(dueDate.getMinutes() + MINUTES_UNTIL_DUE); + }); + + it("should render in long form", () => { + const { wrapper } = setup(dueDate); + + const expectedResult = `${wrapper.vm.$t( + "components.atoms.VCustomChipTimeRemaining.hintDueTime" + )}${MINUTES_UNTIL_DUE - 1} ${wrapper.vm.$t( + "components.atoms.VCustomChipTimeRemaining.hintMinutes", + MINUTES_UNTIL_DUE - 1 + )}`; + + expect(wrapper.element.textContent).toContain(expectedResult); + }); + + it("should render in shortened form", () => { + const { wrapper } = setup(dueDate, true); + + const expectedResult = `${wrapper.vm.$t( + "components.atoms.VCustomChipTimeRemaining.hintDueTime" + )}${MINUTES_UNTIL_DUE - 1} ${wrapper.vm.$t( + "components.atoms.VCustomChipTimeRemaining.hintMinShort" + )}`; + + expect(wrapper.element.textContent).toContain(expectedResult); + }); + }); + + it("accepts valid type props", () => { + const validTypes = ["warning"]; + const { validator } = VCustomChipTimeRemaining.props.type; + + validTypes.forEach((type) => { + expect(validator(type)).toBe(true); + }); + + expect(validator("wrong type")).toBe(false); + }); + + it("accepts valid dueDate props", () => { + const validDueDates = [ + "2021-06-11T14:00:00.000Z", + "2021-06-07T09:30:00.000Z", + ]; + const { validator } = VCustomChipTimeRemaining.props.dueDate; + + validDueDates.forEach((dueDate) => { + expect(validator(dueDate)).toBe(true); + }); + + expect(validator("wrong due date")).toBe(false); + }); +}); diff --git a/src/components/atoms/vCustomFab.unit.ts b/src/components/atoms/vCustomFab.unit.ts deleted file mode 100644 index b28700fca7..0000000000 --- a/src/components/atoms/vCustomFab.unit.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { mount, MountOptions } from "@vue/test-utils"; -import { mdiPlus, mdiAccountPlus, mdiAccountMultipleMinus } from "@mdi/js"; -import vCustomFab from "./vCustomFab.vue"; -import Vue from "vue"; -import createComponentMocks from "@@/tests/test-utils/componentMocks"; - -const getWrapper = (props: object, options?: object) => { - return mount(vCustomFab as MountOptions, { - ...createComponentMocks({ - i18n: true, - }), - propsData: props, - ...options, - }); -}; - -describe("@/components/atoms/vCustomFab", () => { - describe("when fab is simple (icon only)", () => { - const simpleFab = { - actions: [], - icon: mdiPlus, - }; - - it("should not display title", () => { - const wrapper = getWrapper({ ...simpleFab }); - - expect(wrapper.text()).toStrictEqual(""); - }); - - it("should display icon", () => { - const wrapper = getWrapper({ ...simpleFab }); - const icon = wrapper.find(".v-icon"); - - expect(icon.exists()).toBe(true); - }); - - it("should have default size ", () => { - const wrapper = getWrapper({ ...simpleFab }); - - expect(wrapper.classes()).toContain("v-size--default"); - }); - - it("should have aria label", () => { - const wrapper = getWrapper({ - ...simpleFab, - ariaLabel: "dummy aria label", - }); - - expect(wrapper.element.getAttribute("aria-label")).toContain( - "dummy aria label" - ); - }); - }); - - describe("when fab is extended", () => { - const simpleExtendedFab = { - actions: [], - icon: mdiPlus, - title: "User", - href: "/", - }; - - it("should display title", () => { - const wrapper = getWrapper({ ...simpleExtendedFab }); - - expect(wrapper.text()).toStrictEqual("User"); - }); - - it("should display icon", () => { - const wrapper = getWrapper({ ...simpleExtendedFab }); - const icon = wrapper.find(".v-icon"); - - expect(icon.exists()).toBe(true); - }); - - it("should have small size with extended width", () => { - const wrapper = getWrapper({ ...simpleExtendedFab }); - - expect(wrapper.classes()).toContain("v-size--small"); - expect(wrapper.classes()).toContain("extended-fab"); - }); - - it("should be positioned top right on desktop", () => { - Object.defineProperty(window, "innerWidth", { - writable: true, - configurable: true, - value: 1264, - }); - window.dispatchEvent(new Event("resize")); - - const wrapper = getWrapper({ ...simpleExtendedFab }); - - expect(wrapper.classes()).toContain("v-btn--right"); - expect(wrapper.classes()).toContain("v-btn--top"); - }); - - it("should be positioned bottom right on mobile", () => { - Object.defineProperty(window, "innerWidth", { - writable: true, - configurable: true, - value: 375, - }); - window.dispatchEvent(new Event("resize")); - - const wrapper = getWrapper({ ...simpleExtendedFab }); - - expect(wrapper.classes()).toContain("v-btn--bottom"); - expect(wrapper.classes()).toContain("v-btn--right"); - }); - - it("should become link when href is given", () => { - const wrapper = getWrapper({ ...simpleExtendedFab }); - - const link = wrapper.find("a"); - - expect(link.exists()).toBe(true); - expect(link.attributes().href).toBe("/"); - }); - - // apparantly testing scroll behaviour is not possible with jest - - // it("extended fab should collapse on scroll down", () => {}); - // it("extended fab should extend on scroll up", () => {}); - }); - - describe("when fab is extended with speed dial menu", () => { - const multipleActionsExtendedFab = { - actions: [ - { - label: "Add User", - icon: mdiAccountPlus, - to: "/", - ariaLabel: "Custom Aria Label", - }, - { - label: "Delete User", - icon: mdiAccountMultipleMinus, - to: "/", - }, - ], - icon: mdiPlus, - title: "User", - }; - - it("should become speed dial component when multiple actions are given", () => { - const wrapper = getWrapper({ ...multipleActionsExtendedFab }); - - expect(wrapper.classes()).toContain("v-speed-dial"); - }); - - it("should set speed dial menu direction to bottom on desktop", () => { - Object.defineProperty(window, "innerWidth", { - writable: true, - configurable: true, - value: 1264, - }); - window.dispatchEvent(new Event("resize")); - const wrapper = getWrapper({ ...multipleActionsExtendedFab }); - - expect(wrapper.classes()).toContain("v-speed-dial--direction-bottom"); - }); - - it("should set speed dial menu direction to top on mobile", () => { - Object.defineProperty(window, "innerWidth", { - writable: true, - configurable: true, - value: 375, - }); - window.dispatchEvent(new Event("resize")); - - const wrapper = getWrapper({ ...multipleActionsExtendedFab }); - - expect(wrapper.classes()).toContain("v-speed-dial--direction-top"); - }); - - it("should have aria-labels", async () => { - const wrapper = getWrapper({ ...multipleActionsExtendedFab }); - wrapper.setData({ isSpeedDialExpanded: true }); - await wrapper.vm.$nextTick(); - const actionButtons = wrapper.findAll(".fab-action").wrappers; - - expect(actionButtons[0].element.getAttribute("aria-label")).toContain( - "Custom Aria Label" - ); - expect(actionButtons[1].element.getAttribute("aria-label")).toContain( - "Delete User" - ); - }); - - it.todo("should open speed dial menu on click"); - - it.todo("should collapse when speed dial menu is opened"); - - it.todo("should show overlay when speed dial menu is opened"); - }); -}); diff --git a/src/components/atoms/vCustomFab.vue b/src/components/atoms/vCustomFab.vue deleted file mode 100644 index 6b8adbdbe1..0000000000 --- a/src/components/atoms/vCustomFab.vue +++ /dev/null @@ -1,229 +0,0 @@ - - - - - diff --git a/src/components/atoms/vCustomSwitch.unit.ts b/src/components/atoms/vCustomSwitch.unit.ts deleted file mode 100644 index 2297e95918..0000000000 --- a/src/components/atoms/vCustomSwitch.unit.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { mount, MountOptions } from "@vue/test-utils"; -import vCustomSwitch from "./vCustomSwitch.vue"; -import Vue from "vue"; -import createComponentMocks from "@@/tests/test-utils/componentMocks"; - -describe("vCustomSwitch", () => { - it("should take property value true", () => { - const wrapper = mount(vCustomSwitch as MountOptions, { - ...createComponentMocks({ - i18n: true, - }), - propsData: { - value: true, - label: "mock label", - }, - }); - const customSwitch = wrapper.find("input").element as HTMLInputElement; - expect(customSwitch.checked).toBeTruthy(); - }); - it("should take property value false", () => { - const wrapper = mount(vCustomSwitch as MountOptions, { - ...createComponentMocks({ - i18n: true, - }), - propsData: { - value: false, - label: "mock label", - }, - }); - const customSwitch = wrapper.find("input").element as HTMLInputElement; - expect(customSwitch.checked).toBeFalsy(); - }); - - it("should display externally changing value", async () => { - const wrapper = mount(vCustomSwitch as MountOptions, { - ...createComponentMocks({ - i18n: true, - }), - propsData: { - value: true, - label: "mock label", - }, - }); - const customSwitch = wrapper.find("input").element as HTMLInputElement; - expect(customSwitch.checked).toBeTruthy(); - - await wrapper.setProps({ value: false }); - expect(customSwitch.checked).toBeFalsy(); - - await wrapper.setProps({ value: true }); - expect(customSwitch.checked).toBeTruthy(); - }); - - it("should show the label", () => { - const wrapper = mount(vCustomSwitch as MountOptions, { - ...createComponentMocks({ - i18n: true, - }), - propsData: { - value: false, - label: "mock label", - }, - }); - const customSwitch = wrapper.find("label"); - expect(customSwitch.text()).toBe("mock label"); - }); - - it("should emit events", async () => { - const wrapper = mount(vCustomSwitch as MountOptions, { - ...createComponentMocks({ - i18n: true, - }), - propsData: { - value: false, - label: "mock label", - }, - }); - const customSwitch = wrapper.find("input"); - customSwitch.trigger("click"); - await wrapper.vm.$nextTick(); - - let emitted = wrapper.emitted(); - expect(emitted["input-changed"]).toHaveLength(1); - expect( - emitted["input-changed"] && emitted["input-changed"][0] - ).toHaveLength(1); - expect( - emitted["input-changed"] && - emitted["input-changed"][0] && - emitted["input-changed"][0][0] - ).toBeTruthy(); - - customSwitch.trigger("click"); - await wrapper.vm.$nextTick(); - - emitted = wrapper.emitted(); - expect(emitted["input-changed"]).toHaveLength(2); - expect( - emitted["input-changed"] && emitted["input-changed"][1] - ).toHaveLength(1); - expect( - emitted["input-changed"] && - emitted["input-changed"][1] && - emitted["input-changed"][1][0] - ).toBeFalsy(); - }); -}); diff --git a/src/components/atoms/vCustomSwitch.vue b/src/components/atoms/vCustomSwitch.vue deleted file mode 100644 index 5726eec8f9..0000000000 --- a/src/components/atoms/vCustomSwitch.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - diff --git a/src/components/atoms/vRoomAvatar.unit.ts b/src/components/atoms/vRoomAvatar.unit.ts index 8f2ce9d239..8758af795d 100644 --- a/src/components/atoms/vRoomAvatar.unit.ts +++ b/src/components/atoms/vRoomAvatar.unit.ts @@ -1,6 +1,10 @@ -import { mount, Wrapper } from "@vue/test-utils"; +import { mount } from "@vue/test-utils"; import vRoomAvatar from "./vRoomAvatar.vue"; -import createComponentMocks from "@@/tests/test-utils/componentMocks"; +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; +import { createMock } from "@golevelup/ts-jest"; const mockData = { id: "456", @@ -15,46 +19,43 @@ const mockData = { href: "/rooms/456", }; -const propsData = { - item: mockData, - size: "4em", - showBadge: true, - draggable: true, -}; - -const getWrapper = (props: object, options?: object): Wrapper => { - return mount(vRoomAvatar, { - ...createComponentMocks({ - i18n: true, - }), - propsData: props, - ...options, - }); -}; - describe("vRoomAvatar", () => { + const setup = (optionalProps: object = {}) => { + const wrapper = mount(vRoomAvatar, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + props: { + item: mockData, + size: "4em", + showBadge: true, + draggable: true, + ...optionalProps, + }, + }); + return { wrapper }; + }; beforeEach(() => { window.location.pathname = ""; }); it("should display the title but NOT the date title", () => { - const wrapper = getWrapper({ ...propsData }); + const { wrapper } = setup(); const labelElement = wrapper.find(".subtitle").element as HTMLElement; expect(labelElement).toBeTruthy(); - expect(labelElement.innerHTML.trim()).toContain("Bio 12c"); - expect(labelElement.innerHTML.trim()).not.toContain("2019/2020"); + expect(labelElement.innerHTML).toContain("Bio 12c"); + expect(labelElement.innerHTML).not.toContain("2019/2020"); }); - it("should NOT display the title", () => { - const wrapper = getWrapper({ ...propsData, condenseLayout: true }); - const labelElement = wrapper.find(".subtitle").element as HTMLElement; + it("should NOT display the title", async () => { + const { wrapper } = setup({ condenseLayout: true }); - expect(labelElement).toBeFalsy(); + expect(wrapper.find(".subtitle").exists()).toBeFalsy(); }); it("should display the short title", () => { - const wrapper = getWrapper(propsData); + const { wrapper } = setup(); const shortLabelElement = wrapper.find(".single-avatar") .element as HTMLElement; @@ -62,68 +63,71 @@ describe("vRoomAvatar", () => { expect(shortLabelElement.innerHTML).toStrictEqual("Bi"); }); - it("should display the badge", () => { - const wrapper = getWrapper({ - item: { ...mockData, notification: true }, - size: "4em", - showBadge: true, - }); - const badgeElement = wrapper.find(".badge-component"); + it("should display the badge", async () => { + const { wrapper } = setup({ item: { ...mockData, notification: true } }); + const badgeElement = wrapper.findComponent({ name: "VBadge" }); - expect(badgeElement).toBeTruthy(); - expect(badgeElement.vm.$props.value).toBeTruthy(); - expect(badgeElement.vm.$data.isActive).toBeTruthy(); + expect(badgeElement.props().modelValue).toBe(true); }); it("should NOT display the badge", () => { - const wrapper = getWrapper(propsData); - const badgeElement = wrapper.find(".badge-component"); + const { wrapper } = setup(); + const badgeElement = wrapper.findComponent({ name: "VBadge" }); - expect(badgeElement).toBeTruthy(); - expect(badgeElement.vm.$props.value).toBeFalsy(); - expect(badgeElement.vm.$data.isActive).toBeFalsy(); + expect(badgeElement.props().modelValue).toBe(false); }); it("should display the correct color and size", async () => { - const wrapper = getWrapper(propsData); - const avatarComponent = wrapper.find(".v-avatar"); + const { wrapper } = setup(); + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); - expect(avatarComponent).toBeTruthy(); - expect(avatarComponent.vm.$props.color).toStrictEqual("#ffffff"); - expect(avatarComponent.vm.$props.size).toStrictEqual("4em"); + expect(avatarComponent.props().color).toStrictEqual("#ffffff"); + expect(avatarComponent.props().size).toStrictEqual("4em"); }); it("should redirect to room page", async () => { - const location = window.location; - const wrapper = getWrapper(propsData); - const avatarComponent = wrapper.find(".v-avatar"); + Object.defineProperty(window, "location", { + set: jest.fn(), + get: () => createMock(), + }); + const locationSpy = jest.spyOn(window, "location", "set"); - avatarComponent.trigger("click"); - expect(location.pathname).toStrictEqual("/rooms/456"); + const { wrapper } = setup(); + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); + + await avatarComponent.trigger("click"); + + expect(locationSpy).toHaveBeenCalledWith(mockData.href); }); it("should redirect to room page if keyboard event triggered", async () => { - const location = window.location; - const wrapper = getWrapper(propsData); - const avatarComponent = wrapper.find(".v-avatar"); + Object.defineProperty(window, "location", { + set: jest.fn(), + get: () => createMock(), + }); + const locationSpy = jest.spyOn(window, "location", "set"); - avatarComponent.trigger("keypress.enter"); - expect(location.pathname).toStrictEqual("/rooms/456"); + const { wrapper } = setup(); + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); + + await avatarComponent.trigger("keypress.enter"); + + expect(locationSpy).toHaveBeenCalledWith(mockData.href); }); it("should not redirect to room page if condenseLayout props is true", async () => { - const location = window.location; - const wrapper = getWrapper({ - item: mockData, - size: "4em", - showBadge: true, - draggable: true, - condenseLayout: true, + Object.defineProperty(window, "location", { + set: jest.fn(), + get: () => createMock(), }); - const avatarComponent = wrapper.find(".v-avatar"); + const locationSpy = jest.spyOn(window, "location", "set"); + const { wrapper } = setup({ condenseLayout: true }); + + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); - avatarComponent.trigger("click"); - expect(location.pathname).toStrictEqual(""); + await avatarComponent.trigger("click"); + + expect(locationSpy).not.toHaveBeenCalled(); }); it("should display the title AND the date title", () => { @@ -139,135 +143,103 @@ describe("vRoomAvatar", () => { searchText: "History 2015-2018", isArchived: true, }, - size: "4em", - showBadge: true, - draggable: true, }; - const wrapper = getWrapper({ ...propData }); + const { wrapper } = setup(propData); const element = wrapper.find(".subtitle").element as HTMLElement; expect(element).toBeTruthy(); - expect(element.innerHTML.trim()).toContain("History"); - expect(element.innerHTML.trim()).toContain("2015-2018"); + expect(element.innerHTML).toContain("History"); + expect(element.innerHTML).toContain("2015-2018"); }); describe("drag and drop", () => { it("should emit 'dragStart' event when it started dragging", async () => { - const wrapper = getWrapper(propsData); - const avatarComponent = wrapper.find(".v-avatar"); - - expect(wrapper.vm.$data.isDragging).toBe(false); - avatarComponent.trigger("dragstart"); - await wrapper.vm.$nextTick(); - expect(wrapper.vm.$data.isDragging).toBe(true); - const emitted = wrapper.emitted(); - - expect(emitted["startDrag"]).toHaveLength(1); - expect(emitted["startDrag"] && emitted["startDrag"][0][0]).toStrictEqual( - mockData - ); + const { wrapper } = setup(); + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); + + await avatarComponent.trigger("dragstart"); + const startDragEvent = wrapper.emitted("startDrag"); + + expect(wrapper.vm.isDragging).toBe(true); + expect(startDragEvent).toHaveLength(1); + expect(startDragEvent && startDragEvent[0][0]).toStrictEqual(mockData); }); it("should emit 'drop' event when an element dropped onto it", async () => { - const wrapper = getWrapper(propsData); - const avatarComponent = wrapper.find(".v-avatar"); + const { wrapper } = setup(); + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); - avatarComponent.trigger("drop"); - await wrapper.vm.$nextTick(); - const emitted = wrapper.emitted(); + await avatarComponent.trigger("drop"); - expect(emitted["drop"]).toHaveLength(1); + expect(wrapper.emitted()).toHaveProperty("drop"); }); it("should NOT emit 'dragStart' event if 'draggable' prop is set false", async () => { - const wrapper = getWrapper({ - item: mockData, - size: "4em", - showBadge: true, - draggable: false, - }); - const avatarComponent = wrapper.find(".v-avatar"); + const { wrapper } = setup({ draggable: false }); + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); - avatarComponent.trigger("dragstart"); - await wrapper.vm.$nextTick(); - const emitted = wrapper.emitted(); + await avatarComponent.trigger("dragstart"); + const startDragEvent = wrapper.emitted("startDrag"); - expect(emitted["startDrag"]).toBe(undefined); + expect(startDragEvent).toBe(undefined); }); it("should emit 'dragenter' event when draging over component", async () => { - const wrapper = getWrapper(propsData); - const avatarComponent = wrapper.find(".v-avatar"); - - avatarComponent.trigger("dragenter"); - await wrapper.vm.$nextTick(); + const { wrapper } = setup(); + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); - expect(wrapper.vm.hovered).toBeTruthy(); - expect(wrapper.vm.isDragging).toBeFalsy(); - }); - - it("should emit 'dragleave' event when draging over component", async () => { - const wrapper = getWrapper(propsData); - const avatarComponent = wrapper.find(".v-avatar"); - - avatarComponent.trigger("dragleave"); - await wrapper.vm.$nextTick(); + await avatarComponent.trigger("dragenter"); - expect(wrapper.vm.hovered).toBeFalsy(); + expect(wrapper.vm.isDragging).toBe(false); }); it("should emit 'dragend' event when draging ended", async () => { - const wrapper = getWrapper(propsData); - const avatarComponent = wrapper.find(".v-avatar"); + const { wrapper } = setup(); + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); - avatarComponent.trigger("dragend"); - await wrapper.vm.$nextTick(); - const emitted = wrapper.emitted(); + await avatarComponent.trigger("dragend"); - expect(emitted["dragend"]).toHaveLength(1); - expect(wrapper.vm.isDragging).toBeFalsy(); + expect(wrapper.vm.isDragging).toBe(false); + expect(wrapper.emitted()).toHaveProperty("dragend"); }); }); describe("on long running course copies", () => { - const setup = () => { - const propData = { - item: { - id: "123", - title: "History (1)", - shortTitle: "Hi", - displayColor: "#EF6C00", - startDate: "2023-01-30T22:00:00.000Z", - untilDate: "2023-02-15T22:00:00.000Z", - copyingSince: "2023-01-30T22:00:00.000Z", - searchText: "History (1)", - isArchived: true, - }, - size: "4em", - showBadge: true, - draggable: true, - }; - - const wrapper = getWrapper({ ...propData }); - return wrapper; + const longRunningCourseProps = { + item: { + id: "123", + title: "History (1)", + shortTitle: "Hi", + displayColor: "#EF6C00", + startDate: "2023-01-30T22:00:00.000Z", + untilDate: "2023-02-15T22:00:00.000Z", + copyingSince: "2023-01-30T22:00:00.000Z", + searchText: "History (1)", + isArchived: true, + }, }; it("should display info and not title", () => { - const wrapper = setup(); + const { wrapper } = setup(longRunningCourseProps); + const element = wrapper.find(".subtitle").element as HTMLElement; - expect(element.innerHTML.trim()).toContain("Kurs wird erstellt"); - expect(element.className).toContain("grey--text"); - expect(element.className).toContain("text--darken-1"); + expect(element.innerHTML.trim()).toContain( + "components.molecules.copyResult.courseCopy.info" + ); + expect(element.className).toContain("text-grey"); + expect(element.className).toContain("text-darken-1"); }); it("should display avatar in grey", () => { - const wrapper = setup(); - const element = wrapper.find(".v-avatar").element as HTMLElement; + const { wrapper } = setup(longRunningCourseProps); - expect(element.className.split(" ")).toContain("grey"); - expect(element.className.split(" ")).toContain("lighten-2"); + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); + + expect(avatarComponent.attributes().class.split(" ")).toContain( + "grey-lighten-2" + ); }); }); }); diff --git a/src/components/atoms/vRoomAvatar.vue b/src/components/atoms/vRoomAvatar.vue index 6bfc0f1c38..b9ceacf9f0 100644 --- a/src/components/atoms/vRoomAvatar.vue +++ b/src/components/atoms/vRoomAvatar.vue @@ -13,20 +13,18 @@ - + diff --git a/src/components/atoms/vRoomEmptyAvatar.unit.ts b/src/components/atoms/vRoomEmptyAvatar.unit.ts index 707ddaa3ea..47b6ae9658 100644 --- a/src/components/atoms/vRoomEmptyAvatar.unit.ts +++ b/src/components/atoms/vRoomEmptyAvatar.unit.ts @@ -1,48 +1,49 @@ -import { mount, MountOptions } from "@vue/test-utils"; +import { mount } from "@vue/test-utils"; import vRoomEmptyAvatar from "./vRoomEmptyAvatar.vue"; -import Vue from "vue"; -import createComponentMocks from "@@/tests/test-utils/componentMocks"; - -const propsData = { - size: "4em", -}; - -const getWrapper = (props: object, options?: object) => { - return mount(vRoomEmptyAvatar as MountOptions, { - ...createComponentMocks({ - i18n: true, - }), - propsData: props, - ...options, - }); -}; +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; +import { nextTick } from "vue"; describe("vRoomEmptyAvatar", () => { + const setup = () => { + const wrapper = mount(vRoomEmptyAvatar, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + propsData: { + size: "4em", + }, + }); + return { wrapper }; + }; + it("should have the correct size prop", () => { - const wrapper = getWrapper(propsData); - const avatarComponent = wrapper.find(".avatar-component-empty"); + const { wrapper } = setup(); + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); + expect(avatarComponent).toBeTruthy(); - expect(avatarComponent.vm.$props.size).toStrictEqual("4em"); + expect(avatarComponent.props().size).toStrictEqual("4em"); }); it("should emit 'drop' event when an element drops onto it", async () => { - const wrapper = getWrapper(propsData); - const avatarComponent = wrapper.find(".avatar-component-empty"); + const { wrapper } = setup(); + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); avatarComponent.trigger("drop"); - await wrapper.vm.$nextTick(); - const emitted = wrapper.emitted(); + await nextTick(); - expect(emitted["drop"]).toHaveLength(1); + expect(wrapper.emitted()).toHaveProperty("drop"); }); it("should change its class name while 'drag' events triggered", async () => { - const wrapper = getWrapper(propsData); - const avatarComponent = wrapper.find(".avatar-component-empty"); + const { wrapper } = setup(); + const avatarComponent = wrapper.findComponent({ name: "VAvatar" }); expect(avatarComponent.element.className).not.toContain("hovered-avatar"); avatarComponent.trigger("dragenter"); - await wrapper.vm.$nextTick(); + await nextTick(); expect(avatarComponent.element.className).toContain("hovered-avatar"); avatarComponent.trigger("dragleave"); diff --git a/src/components/atoms/vRoomEmptyAvatar.vue b/src/components/atoms/vRoomEmptyAvatar.vue index 1cbf42169b..0d63a729a6 100644 --- a/src/components/atoms/vRoomEmptyAvatar.vue +++ b/src/components/atoms/vRoomEmptyAvatar.vue @@ -3,10 +3,9 @@ diff --git a/src/components/base/BaseModal.unit.js b/src/components/base/BaseModal.unit.js index a790a47dbb..dd8b3fe092 100644 --- a/src/components/base/BaseModal.unit.js +++ b/src/components/base/BaseModal.unit.js @@ -1,87 +1,26 @@ import { mount } from "@vue/test-utils"; import BaseModal from "./BaseModal"; - -const modal = { - ...createComponentMocks({ - i18n: true, - vuetify: true, - }), - data: () => ({ active: false }), - template: ` - -
- - - - - -
-
- `, - components: { BaseModal }, -}; +import { createTestingVuetify } from "@@/tests/test-utils/setup"; describe("@/components/base/BaseModal", () => { - it( - ...rendersSlotContent(BaseModal, ["default"], { - propsData: { - active: true, + const setup = (options = {}) => { + const wrapper = mount(BaseModal, { + global: { + plugins: [createTestingVuetify()], }, - }) - ); - - it("changing the active property should open and close the modal", async () => { - const wrapper = mount(modal); - - expect(wrapper.find("#btn-close").exists()).toBe(false); - wrapper.vm.active = true; - await wrapper.vm.$nextTick(); - expect(wrapper.find("#btn-close").exists()).toBe(true); - }); - - it("pressing the ok button should close the modal", async () => { - const wrapper = mount(modal, { - ...createComponentMocks({ stubs: { transition: true } }), + ...options, }); - expect(wrapper.find("#btn-close").exists()).toBe(false); - wrapper.vm.active = true; - await wrapper.vm.$nextTick(); - expect(wrapper.find("#btn-close").exists()).toBe(true); - wrapper.find("#btn-close").trigger("click"); - await wrapper.vm.$nextTick(); - expect(wrapper.find("#btn-close").exists()).toBe(false); - }); + return { wrapper }; + }; - it("pressing outside the model content should emit onBackdropClick event", async () => { - const wrapper = mount(modal); + it("changing the active property should open and close the modal", async () => { + const { wrapper } = setup(); - wrapper.vm.active = true; - await wrapper.vm.$nextTick(); - expect(wrapper.find("#btn-close").exists()).toBe(true); - wrapper.find(".base-modal-wrapper").trigger("click"); - await wrapper.vm.$nextTick(); - expect( - wrapper.findComponent(BaseModal).emitted("onBackdropClick") - ).toHaveLength(1); - }); + expect(wrapper.findComponent({ name: "v-card" }).exists()).toBe(false); - it("pressing outside the model content should close the modal", async () => { - const wrapper = mount(modal); + await wrapper.setProps({ active: true }); - wrapper.vm.active = true; - await wrapper.vm.$nextTick(); - expect(wrapper.find("#btn-close").exists()).toBe(true); - wrapper.find(".base-modal-wrapper").trigger("click"); - await wrapper.vm.$nextTick(); - expect(wrapper.find("#btn-close").exists()).toBe(false); + expect(wrapper.findComponent({ name: "v-card" }).exists()).toBe(true); }); }); diff --git a/src/components/base/BaseModal.vue b/src/components/base/BaseModal.vue index 31deaa6850..9b93c9f32a 100644 --- a/src/components/base/BaseModal.vue +++ b/src/components/base/BaseModal.vue @@ -1,131 +1,74 @@ diff --git a/src/components/base/_globals.js b/src/components/base/_globals.js deleted file mode 100644 index 048633ed6a..0000000000 --- a/src/components/base/_globals.js +++ /dev/null @@ -1,51 +0,0 @@ -// Globally register all base components for convenience, because they -// will be used very frequently. Components are registered using the -// PascalCased version of their file name. - -import Vue from "vue"; -import upperFirst from "lodash/upperFirst"; -import camelCase from "lodash/camelCase"; - -export const mountBaseComponents = ( - globalComponentFiles, - getComponentConfig -) => { - for (const fileName of globalComponentFiles) { - // Get Component Name - const componentName = fileName.match(/(\w+).vue$/)[1]; - // Is naming scheme valid? - if (componentName !== upperFirst(camelCase(componentName))) { - throw new Error(`${fileName} is not in PascalCase.`); - } - // Is it a Entrypoint? - const isEntrypoint = - fileName.startsWith(`./${componentName}.vue`) || // standalone component - fileName.startsWith(`./${componentName}/`); // or root component of folder - // Globally register the component - if (isEntrypoint) { - const componentConfig = getComponentConfig(fileName); - Vue.component(componentName, componentConfig.default || componentConfig); - } - } -}; - -const mountWithWebpack = () => { - // https://webpack.js.org/guides/dependency-management/#require-context - const requireComponent = require.context( - // Look for files in the current directory - ".", - // Do not look in subdirectories - true, - // Only include "Base" prefixed .vue files - /Base\w+\.vue$/ - ); - - mountBaseComponents(requireComponent.keys(), (fileName) => - requireComponent(fileName) - ); -}; - -if (process.env.JEST_WORKER_ID === undefined) { - // don't use for tests (jest) - mountWithWebpack(); -} diff --git a/src/components/base/components.js b/src/components/base/components.js new file mode 100644 index 0000000000..f30cc36bf4 --- /dev/null +++ b/src/components/base/components.js @@ -0,0 +1,17 @@ +import BaseDialog from "@/components/base/BaseDialog/BaseDialog.vue"; +import BaseInput from "@/components/base/BaseInput/BaseInput.vue"; +import BaseInputCheckbox from "@/components/base/BaseInput/BaseInputCheckbox.vue"; +import BaseInputDefault from "@/components/base/BaseInput/BaseInputDefault.vue"; +import BaseLink from "@/components/base/BaseLink.vue"; +import BaseModal from "@/components/base/BaseModal.vue"; +import BaseQrCode from "@/components/base/BaseQrCode.vue"; + +export const mountBaseComponents = (app) => { + app.component("BaseDialog", BaseDialog); + app.component("BaseInput", BaseInput); + app.component("BaseInputCheckbox", BaseInputCheckbox); + app.component("BaseInputDefault", BaseInputDefault); + app.component("BaseLink", BaseLink); + app.component("BaseModal", BaseModal); + app.component("BaseQrCode", BaseQrCode); +}; diff --git a/src/components/copy-result-modal/CopyResultModal.unit.ts b/src/components/copy-result-modal/CopyResultModal.unit.ts index ae04d86482..6e2bcc4e45 100644 --- a/src/components/copy-result-modal/CopyResultModal.unit.ts +++ b/src/components/copy-result-modal/CopyResultModal.unit.ts @@ -1,42 +1,44 @@ import { CopyApiResponseTypeEnum } from "@/serverApi/v3"; -import createComponentMocks from "@@/tests/test-utils/componentMocks"; import vCustomDialog from "@/components/organisms/vCustomDialog.vue"; -import { mount, MountOptions } from "@vue/test-utils"; +import { mount } from "@vue/test-utils"; import CopyResultModal from "./CopyResultModal.vue"; -import Vue from "vue"; -import { envConfigModule } from "@/store"; import setupStores from "@@/tests/test-utils/setupStores"; import EnvConfigModule from "@/store/env-config"; +import { envConfigModule } from "@/store"; import { Envs } from "@/store/types/env-config"; +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; -const geoGebraItem = { +const mockGeoGebraItem = { title: "GeoGebra Element Title", type: CopyApiResponseTypeEnum.LessonContentGeogebra, }; -const etherpadItem = { +const mockEtherpadItem = { title: "Etherpad Element Title", type: CopyApiResponseTypeEnum.LessonContentEtherpad, }; -const nexboardItem = { +const mockNexboardItem = { title: "Nexboard Element Title", type: CopyApiResponseTypeEnum.LessonContentNexboard, }; -const courseGroupItem = { +const mockCourseGroupItem = { title: "CourseGroup Group Example", type: CopyApiResponseTypeEnum.CoursegroupGroup, }; -const fileItem = { +const mockFileItem = { title: "File Error Example", type: CopyApiResponseTypeEnum.File, }; const mockResultItems = ( elements = [ - geoGebraItem, - etherpadItem, - courseGroupItem, - nexboardItem, - fileItem, + mockGeoGebraItem, + mockEtherpadItem, + mockNexboardItem, + mockCourseGroupItem, + mockFileItem, ] ) => { return [ @@ -50,25 +52,23 @@ const mockResultItems = ( ]; }; -const getWrapper = (props?: any) => { - const wrapper = mount(CopyResultModal as MountOptions, { - ...createComponentMocks({ - i18n: true, - }), - propsData: { - isLoading: false, - copyResultItems: mockResultItems(), - ...props, - }, - }); +describe("@/components/copy-result-modal/CopyResultModal", () => { + const createWrapper = (options = {}) => { + const wrapper = mount(CopyResultModal, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + props: { + isOpen: false, + copyResultItems: mockResultItems(), + ...options, + }, + }); - return wrapper; -}; + return wrapper; + }; -describe("@/components/copy-result-modal/CopyResultModal", () => { beforeAll(() => { - // Avoids console warnings "[Vuetify] Unable to locate target [data-app]" - document.body.setAttribute("data-app", "true"); setupStores({ envConfigModule: EnvConfigModule, }); @@ -80,7 +80,7 @@ describe("@/components/copy-result-modal/CopyResultModal", () => { describe("basic functions", () => { it("Should render component", () => { - const wrapper = getWrapper(); + const wrapper = createWrapper(); expect(wrapper.findComponent(CopyResultModal).exists()).toBe(true); }); @@ -88,33 +88,42 @@ describe("@/components/copy-result-modal/CopyResultModal", () => { describe("isOpen", () => { it("should be closed by default", () => { - const wrapper = getWrapper(); + const wrapper = createWrapper(); + + const dialog = wrapper.findComponent(vCustomDialog); + const title = dialog.findComponent('[data-testid="dialog-title"'); - expect(wrapper.find(".v-dialog__content").exists()).toBe(false); + expect(dialog.vm.isOpen).toBe(false); + expect(title.exists()).toBe(false); }); it("should be open when is-open property is true", () => { - const wrapper = getWrapper({ isOpen: true }); + const wrapper = createWrapper({ isOpen: true }); + + const dialog = wrapper.findComponent(vCustomDialog); + const title = dialog.findComponent('[data-testid="dialog-title"'); - expect(wrapper.find(".v-dialog__content").exists()).toBe(true); + expect(dialog.vm.isOpen).toBe(true); + expect(title.exists()).toBe(true); }); }); describe("title", () => { it("should show partial-title when copy was partially successful", () => { - const wrapper = getWrapper({ isOpen: true }); + const wrapper = createWrapper({ isOpen: true }); - const headline = wrapper.find('[data-testid="dialog-title"]').text(); + const dialog = wrapper.findComponent(vCustomDialog); + const headline = dialog + .findComponent('[data-testid="dialog-title"]') + .text(); - expect(headline).toContain( - wrapper.vm.$i18n.t("components.molecules.copyResult.title.partial") - ); + expect(headline).toBe("components.molecules.copyResult.title.partial"); }); }); describe("dialog-closed", () => { - it("should forward the dialog-closed event of the wrapped dialog", async () => { - const wrapper = getWrapper({ isOpen: true }); + it("should forward the dialog-closed event of the wrapped dialog", () => { + const wrapper = createWrapper({ isOpen: true }); const dialog = wrapper.findComponent(vCustomDialog); dialog.vm.$emit("dialog-closed"); @@ -127,39 +136,41 @@ describe("@/components/copy-result-modal/CopyResultModal", () => { it("should render coursefiles info if root item is a Course and has no failed file ", () => { const copyResultItems = mockResultItems([]); - const wrapper = getWrapper({ + const wrapper = createWrapper({ isOpen: true, copyResultItems, copyResultRootItemType: CopyApiResponseTypeEnum.Course, }); - expect( - wrapper.find('[data-testid="copy-result-notifications"]').text() - ).toContain( - wrapper.vm.$i18n.t("components.molecules.copyResult.courseFiles.info") + const dialog = wrapper.findComponent(vCustomDialog); + const content = dialog.findComponent(".v-card-text").text(); + + expect(content).toContain( + "components.molecules.copyResult.courseFiles.info" ); }); it("should render ctl tools info if root item is a Course and has no failed file ", () => { const copyResultItems = mockResultItems([]); envConfigModule.setEnvs({ FEATURE_CTL_TOOLS_TAB_ENABLED: true } as Envs); - const wrapper = getWrapper({ + const wrapper = createWrapper({ isOpen: true, copyResultItems, copyResultRootItemType: CopyApiResponseTypeEnum.Course, }); - expect( - wrapper.find('[data-testid="copy-result-notifications"]').text() - ).toContain( - wrapper.vm.$i18n.t("components.molecules.copyResult.ctlTools.info") + const dialog = wrapper.findComponent(vCustomDialog); + const content = dialog.findComponent(".v-card-text").text(); + + expect(content).toContain( + "components.molecules.copyResult.ctlTools.info" ); }); describe("when root item is a Course, has no failed file and CTL_TOOLS_COPY feature flag is enabled", () => { const setup = () => { const copyResultItems = mockResultItems([]); - const wrapper = getWrapper({ + const wrapper = createWrapper({ isOpen: true, copyResultItems, copyResultRootItemType: CopyApiResponseTypeEnum.Course, @@ -175,49 +186,50 @@ describe("@/components/copy-result-modal/CopyResultModal", () => { } as Envs); const { wrapper } = setup(); - expect( - wrapper.find('[data-testid="copy-result-notifications"]').text() - ).toContain( - wrapper.vm.$i18n.t( - "components.molecules.copyResult.ctlTools.withFeature.info" - ) + const dialog = wrapper.findComponent(vCustomDialog); + const content = dialog.findComponent(".v-card-text").text(); + + expect(content).toContain( + "components.molecules.copyResult.ctlTools.withFeature.info" ); }); }); it("should merge file error and coursefiles info if root item is a Course and has a failed file ", () => { - const copyResultItems = mockResultItems([fileItem]); + const copyResultItems = mockResultItems([mockFileItem]); - const wrapper = getWrapper({ + const wrapper = createWrapper({ isOpen: true, copyResultItems, copyResultRootItemType: CopyApiResponseTypeEnum.Course, }); - expect( - wrapper.find('[data-testid="copy-result-notifications"]').text() - ).toContain( - wrapper.vm.$i18n.t("components.molecules.copyResult.courseFiles.info") + + const dialog = wrapper.findComponent(vCustomDialog); + const content = dialog.findComponent(".v-card-text").text(); + + expect(content).toContain( + "components.molecules.copyResult.courseFiles.info" + " " + - wrapper.vm.$i18n.t("components.molecules.copyResult.fileCopy.error") + "components.molecules.copyResult.fileCopy.error" ); }); it.each([[CopyApiResponseTypeEnum.Lesson], [CopyApiResponseTypeEnum.Task]])( "should render file error info if root item is a %s and has a failed file", (copyResultRootItemType) => { - const copyResultItems = mockResultItems([fileItem]); + const copyResultItems = mockResultItems([mockFileItem]); - const wrapper = getWrapper({ + const wrapper = createWrapper({ isOpen: true, copyResultItems, copyResultRootItemType, }); - expect( - wrapper.find('[data-testid="copy-result-notifications"]').text() - ).toContain( - wrapper.vm.$i18n.t("components.molecules.copyResult.fileCopy.error") + const dialog = wrapper.findComponent(vCustomDialog); + const content = dialog.findComponent(".v-card-text").text(); + + expect(content).toContain( + "components.molecules.copyResult.fileCopy.error" ); } ); @@ -240,11 +252,12 @@ describe("@/components/copy-result-modal/CopyResultModal", () => { }, ]; - const wrapper = getWrapper({ isOpen: true, copyResultItems }); + const wrapper = createWrapper({ isOpen: true, copyResultItems }); + + const dialog = wrapper.findComponent(vCustomDialog); + const content = dialog.findComponent(".v-card-text").text(); - expect( - wrapper.find('[data-testid="copy-result-notifications"]').text() - ).toContain(title); + expect(content).toContain(title); }); }); }); diff --git a/src/components/copy-result-modal/CopyResultModal.vue b/src/components/copy-result-modal/CopyResultModal.vue index a97d8c85e0..90fc81d507 100644 --- a/src/components/copy-result-modal/CopyResultModal.vue +++ b/src/components/copy-result-modal/CopyResultModal.vue @@ -7,15 +7,15 @@ :buttons="['close']" @dialog-closed="onDialogClosed" > -
- {{ $t("components.molecules.copyResult.title.partial") }} -
+ - - diff --git a/src/components/external-tools/configuration/ExternalToolConfigSettings.unit.ts b/src/components/external-tools/configuration/ExternalToolConfigSettings.unit.ts index 189942b31a..0afcb4610c 100644 --- a/src/components/external-tools/configuration/ExternalToolConfigSettings.unit.ts +++ b/src/components/external-tools/configuration/ExternalToolConfigSettings.unit.ts @@ -3,34 +3,26 @@ import { schoolExternalToolConfigurationTemplateFactory, toolParameterFactory, } from "@@/tests/test-utils"; -import createComponentMocks from "@@/tests/test-utils/componentMocks"; -import { MountOptions, shallowMount, Wrapper } from "@vue/test-utils"; -import Vue from "vue"; +import { shallowMount } from "@vue/test-utils"; import ExternalToolConfigSettings from "./ExternalToolConfigSettings.vue"; +import { createTestingI18n } from "@@/tests/test-utils/setup"; describe("ExternalToolConfigSettings", () => { const getWrapper = ( props: { template: ExternalToolConfigurationTemplate; - value: (string | undefined)[]; + modelValue: (string | undefined)[]; } = { template: schoolExternalToolConfigurationTemplateFactory.build(), - value: [], + modelValue: [], } ) => { - document.body.setAttribute("data-app", "true"); - - const wrapper: Wrapper = shallowMount( - ExternalToolConfigSettings as MountOptions, - { - ...createComponentMocks({ - i18n: true, - }), - propsData: { - ...props, - }, - } - ); + const wrapper = shallowMount(ExternalToolConfigSettings, { + global: { + plugins: [createTestingI18n()], + }, + props, + }); return { wrapper, @@ -55,7 +47,7 @@ describe("ExternalToolConfigSettings", () => { const { wrapper } = getWrapper({ template, - value: [], + modelValue: [], }); return { diff --git a/src/components/external-tools/configuration/ExternalToolConfigSettings.vue b/src/components/external-tools/configuration/ExternalToolConfigSettings.vue index 54e03314ad..7901c922d6 100644 --- a/src/components/external-tools/configuration/ExternalToolConfigSettings.vue +++ b/src/components/external-tools/configuration/ExternalToolConfigSettings.vue @@ -3,43 +3,25 @@
- diff --git a/src/components/external-tools/configuration/ExternalToolConfigurator.unit.ts b/src/components/external-tools/configuration/ExternalToolConfigurator.unit.ts index 9b80b8daf2..ae866e1c31 100644 --- a/src/components/external-tools/configuration/ExternalToolConfigurator.unit.ts +++ b/src/components/external-tools/configuration/ExternalToolConfigurator.unit.ts @@ -1,7 +1,5 @@ import * as useExternalToolUtilsComposable from "@/composables/external-tool-mappings.composable"; -import { mount, MountOptions, Wrapper } from "@vue/test-utils"; -import createComponentMocks from "@@/tests/test-utils/componentMocks"; -import Vue from "vue"; +import { VueWrapper, mount } from "@vue/test-utils"; import { schoolExternalToolConfigurationTemplateFactory, schoolExternalToolFactory, @@ -12,9 +10,12 @@ import { } from "@/store/external-tool"; import { ContextExternalTool } from "@/store/external-tool/context-external-tool"; import { BusinessError } from "@/store/types/commons"; -import { I18N_KEY } from "@/utils/inject"; -import { i18nMock } from "@@/tests/test-utils"; import ExternalToolConfigurator from "./ExternalToolConfigurator.vue"; +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; +import { VBtn } from "vuetify/lib/components/index.mjs"; describe("ExternalToolConfigurator", () => { jest @@ -24,28 +25,18 @@ describe("ExternalToolConfigurator", () => { getBusinessErrorTranslationKey: () => "", }); - const getWrapper = (propsData: { + const getWrapper = (props: { templates: ExternalToolConfigurationTemplate[]; configuration?: SchoolExternalTool | ContextExternalTool; error?: BusinessError; loading?: boolean; }) => { - document.body.setAttribute("data-app", "true"); - - const wrapper: Wrapper = mount( - ExternalToolConfigurator as MountOptions, - { - ...createComponentMocks({ - i18n: true, - }), - provide: { - [I18N_KEY.valueOf()]: i18nMock, - }, - propsData: { - ...propsData, - }, - } - ); + const wrapper = mount(ExternalToolConfigurator, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + props, + }); return { wrapper, @@ -67,15 +58,13 @@ describe("ExternalToolConfigurator", () => { templates: [template], }); - const openSelect = async (wrapper: Wrapper) => { + const openSelect = async (wrapper: VueWrapper) => { await wrapper .find('[data-testId="configuration-select"]') .trigger("click"); await wrapper - .find(".menuable__content__active") - .findAll(".v-list-item") - .at(0) + .find(".menuable__content__active .v-list-item:firstChild") .trigger("click"); }; @@ -130,7 +119,9 @@ describe("ExternalToolConfigurator", () => { it("should disable the selection", async () => { const { wrapper } = setup(); - const select = wrapper.find('[data-testId="configuration-select"]'); + const select = wrapper + .findComponent('[data-testId="configuration-select"]') + .get("input"); expect(select.attributes("disabled")).toBeDefined(); }); @@ -138,11 +129,9 @@ describe("ExternalToolConfigurator", () => { it("should display the edited tool in the selection", async () => { const { wrapper, template } = setup(); - const selectionRow = wrapper.find(".row"); + const selectionRow = wrapper.find(".v-autocomplete .v-list-item-title"); - expect(selectionRow.find("span").text()).toEqual( - expect.stringContaining(template.name) - ); + expect(selectionRow.text()).toEqual(template.name); }); }); }); @@ -155,7 +144,9 @@ describe("ExternalToolConfigurator", () => { schoolExternalToolConfigurationTemplateFactory.buildList(1), }); - await wrapper.find('[data-testId="cancel-button"]').vm.$emit("click"); + await wrapper + .findComponent('[data-testId="cancel-button"]') + .trigger("click"); expect(wrapper.emitted("cancel")).toBeDefined(); }); @@ -171,8 +162,9 @@ describe("ExternalToolConfigurator", () => { configuration: schoolExternalToolFactory.build(), }); - wrapper.find('[data-testId="save-button"]').vm.$emit("click"); - await Vue.nextTick(); + await wrapper + .findComponent('[data-testId="save-button"]') + .trigger("click"); expect(wrapper.emitted("save")).toBeDefined(); }); diff --git a/src/components/external-tools/configuration/ExternalToolConfigurator.vue b/src/components/external-tools/configuration/ExternalToolConfigurator.vue index 46668524a5..b1e11484a4 100644 --- a/src/components/external-tools/configuration/ExternalToolConfigurator.vue +++ b/src/components/external-tools/configuration/ExternalToolConfigurator.vue @@ -1,32 +1,34 @@ @@ -41,8 +42,8 @@ import { import { mdiPencilOutline, mdiTrashCanOutline } from "@mdi/js"; import { BoardMenu, - BoardMenuActionEdit, BoardMenuActionDelete, + BoardMenuActionEdit, BoardMenuActionMoveLeft, BoardMenuActionMoveRight, } from "@ui-board"; @@ -77,7 +78,6 @@ export default defineComponent({ }, emits: [ "delete:column", - "move:column-keyboard", "move:column-left", "move:column-right", "update:title", @@ -91,7 +91,7 @@ export default defineComponent({ const isDeleteModalOpen = ref(false); const columnHeader = ref(null); - const { isFocusContained } = useBoardFocusHandler( + const { isFocusContained, isFocusedById } = useBoardFocusHandler( columnId.value, columnHeader ); @@ -107,10 +107,21 @@ export default defineComponent({ stopEditMode(); }; - const onDelete = () => emit("delete:column", props.columnId); + const onDelete = async (confirmation: Promise) => { + const shouldDelete = await confirmation; + if (shouldDelete) { + emit("delete:column", props.columnId); + } + }; const onMoveColumnKeyboard = (event: KeyboardEvent) => { - emit("move:column-keyboard", event.code); + if (event.code === "ArrowLeft") { + emit("move:column-left"); + } else if (event.code === "ArrowRight") { + emit("move:column-right"); + } else { + console.log("not supported key event"); + } }; const onMoveColumnLeft = () => { @@ -140,11 +151,15 @@ export default defineComponent({ onMoveColumnLeft, onMoveColumnRight, onUpdateTitle, + isFocusedById, }; }, }); diff --git a/src/components/feature-board/card/CardHostDetailView.unit.ts b/src/components/feature-board/card/CardHostDetailView.unit.ts index 2f97d00428..5c16a31f8a 100644 --- a/src/components/feature-board/card/CardHostDetailView.unit.ts +++ b/src/components/feature-board/card/CardHostDetailView.unit.ts @@ -1,35 +1,34 @@ import { BoardCard } from "@/types/board/Card"; -import { I18N_KEY } from "@/utils/inject"; import { boardCardFactory, fileElementResponseFactory, } from "@@/tests/test-utils"; -import createComponentMocks from "@@/tests/test-utils/componentMocks"; -import { MountOptions, shallowMount, Wrapper } from "@vue/test-utils"; -import Vue from "vue"; +import { shallowMount } from "@vue/test-utils"; import CardHostDetailView from "./CardHostDetailView.vue"; +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; const CARD_WITH_ELEMENTS: BoardCard = boardCardFactory.build({ elements: [fileElementResponseFactory.build()], }); describe("CardHostDetailView", () => { - let wrapper: Wrapper; - - const setup = (props: { card: BoardCard }) => { + const setup = (props: { card: BoardCard; isOpen: boolean }) => { document.body.setAttribute("data-app", "true"); - wrapper = shallowMount(CardHostDetailView as MountOptions, { - ...createComponentMocks({}), - provide: { - [I18N_KEY.valueOf()]: { t: (key: string) => key }, + const wrapper = shallowMount(CardHostDetailView, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], }, propsData: props, }); + return { wrapper }; }; describe("when component is mounted", () => { it("should be found in dom", () => { - setup({ card: CARD_WITH_ELEMENTS }); + const { wrapper } = setup({ card: CARD_WITH_ELEMENTS, isOpen: true }); expect(wrapper.findComponent(CardHostDetailView).exists()).toBe(true); }); }); diff --git a/src/components/feature-board/card/CardHostDetailView.vue b/src/components/feature-board/card/CardHostDetailView.vue index 82fe26290a..1e045f1e03 100644 --- a/src/components/feature-board/card/CardHostDetailView.vue +++ b/src/components/feature-board/card/CardHostDetailView.vue @@ -1,9 +1,9 @@