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 @@