diff --git a/src/layouts/LoggedIn.unit.ts b/src/layouts/LoggedIn.unit.ts index 3b7e538584..d1168afeb6 100644 --- a/src/layouts/LoggedIn.unit.ts +++ b/src/layouts/LoggedIn.unit.ts @@ -19,6 +19,8 @@ import { h, nextTick } from "vue"; import { VApp } from "vuetify/lib/components/index.mjs"; import LoggedInLayout from "./LoggedIn.layout.vue"; import { Topbar } from "@ui-layout"; +import { createTestingPinia } from "@pinia/testing"; +import setupStores from "@@/tests/test-utils/setupStores"; jest.mock("vue-router", () => ({ useRoute: () => ({ path: "rooms/courses-list" }), @@ -43,12 +45,20 @@ const setup = () => { }, }); + setupStores({ + envConfigModule: EnvConfigModule, + }); + const wrapper = mount(VApp, { slots: { default: h(LoggedInLayout), }, global: { - plugins: [createTestingVuetify(), createTestingI18n()], + plugins: [ + createTestingVuetify(), + createTestingI18n(), + createTestingPinia(), + ], provide: { [AUTH_MODULE_KEY.valueOf()]: authModule, [ENV_CONFIG_MODULE_KEY.valueOf()]: envConfigModule, diff --git a/src/locales/de.ts b/src/locales/de.ts index ab661e990d..7ad1e5987a 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -25,6 +25,7 @@ export default { "common.actions.shareCopy": "Kopie teilen", "common.actions.update": "Aktualisieren", "common.ariaLabel.newTab": "öffnet in einem neuen Tab", + "common.ariaLabel.sameTab": "öffnet im selben Tab", "common.labels.admin": "Admin(s)", "common.labels.birthdate": "Geburtsdatum", "common.labels.birthday": "Geburtsdatum", @@ -97,6 +98,7 @@ export default { "common.medium.chip.deactivated": "Deaktiviert", "common.medium.chip.notLicensed": "Nicht freigeschaltet", "common.medium.chip.incomplete": "Konfiguration unvollständig", + "common.medium.chip.noLongerAvailable": "Nicht mehr verfügbar", "common.medium.information.admin": "Bitte Einstellungen überprüfen.", "common.medium.information.student": "Bitte an eine Lehrkraft wenden.", "common.medium.information.teacher": "Bitte an Schuladministrator:in wenden.", diff --git a/src/locales/en.ts b/src/locales/en.ts index f146b7dc3a..219d3b3c68 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -25,6 +25,7 @@ export default { "common.actions.shareCopy": "Share copy", "common.actions.update": "Update", "common.ariaLabel.newTab": "opens in a new tab", + "common.ariaLabel.sameTab": "opens in the same tab", "common.labels.admin": "", "common.labels.birthdate": "Date of birth", "common.labels.birthday": "Date of Birth", @@ -97,6 +98,7 @@ export default { "common.medium.chip.deactivated": "Disabled", "common.medium.chip.notLicensed": "Not activated", "common.medium.chip.incomplete": "Configuration incomplete", + "common.medium.chip.noLongerAvailable": "No longer available", "common.medium.information.admin": "Please check settings.", "common.medium.information.student": "Please contact a teacher.", "common.medium.information.teacher": diff --git a/src/locales/es.ts b/src/locales/es.ts index cfd02ecde9..9542bf9b30 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -25,6 +25,7 @@ export default { "common.actions.shareCopy": "Compartir copia", "common.actions.update": "Actualizar", "common.ariaLabel.newTab": "se abre en una nueva pestaña", + "common.ariaLabel.sameTab": "se abre en la misma pestaña", "common.labels.admin": "Admin(s)", "common.labels.birthdate": "Fecha de nacimiento", "common.labels.birthday": "Fecha de nacimiento", @@ -97,6 +98,7 @@ export default { "common.medium.chip.deactivated": "Desactivado", "common.medium.chip.notLicensed": "No esta activado", "common.medium.chip.incomplete": "Configuración incompleta", + "common.medium.chip.noLongerAvailable": "Ya no está disponible", "common.medium.information.admin": "Por favor verifique la configuración.", "common.medium.information.student": "Por favor contacte a un maestro.", "common.medium.information.teacher": diff --git a/src/locales/uk.ts b/src/locales/uk.ts index 0bdc71c9f3..5acdea7744 100644 --- a/src/locales/uk.ts +++ b/src/locales/uk.ts @@ -25,6 +25,7 @@ export default { "common.actions.shareCopy": "Поділитися копією", "common.actions.update": "Оновити", "common.ariaLabel.newTab": "відкривається в новій вкладці", + "common.ariaLabel.sameTab": "відкривається в тій же вкладці", "common.labels.admin": "адміністратор(и)", "common.labels.birthdate": "Дата народження", "common.labels.birthday": "Дата народження", @@ -97,6 +98,7 @@ export default { "common.medium.chip.deactivated": "Вимкнено", "common.medium.chip.notLicensed": "Не активовано", "common.medium.chip.incomplete": "Конфігурація не завершена", + "common.medium.chip.noLongerAvailable": "Більше не доступний", "common.medium.information.admin": "Перевірте налаштування.", "common.medium.information.student": "Будь ласка, зверніться до вчителя.", "common.medium.information.teacher": diff --git a/src/modules/data/board/BoardApi.composable.ts b/src/modules/data/board/BoardApi.composable.ts index e05522f8db..1f8493c72a 100644 --- a/src/modules/data/board/BoardApi.composable.ts +++ b/src/modules/data/board/BoardApi.composable.ts @@ -17,6 +17,7 @@ import { CourseRoomsApiFactory, SubmissionContainerElementContentBody, } from "@/serverApi/v3"; +import { BoardContextType } from "@/types/board/BoardContext"; import { AnyContentElement } from "@/types/board/ContentElement"; import { $axios, mapAxiosErrorToResponseError } from "@/utils/api"; import { createApplicationError } from "@/utils/create-application-error.factory"; @@ -185,7 +186,7 @@ export const useBoardApi = () => { }); }; - type ContextInfo = { id: string; name: string }; + type ContextInfo = { id: string; type: BoardContextType; name: string }; const getContextInfo = async ( boardId: string @@ -205,6 +206,7 @@ export const useBoardApi = () => { } return { id: roomResponse.data.roomId, + type: context.type, name: roomResponse.data.title, }; }; diff --git a/src/modules/data/board/BoardPageInformation.composable.ts b/src/modules/data/board/BoardPageInformation.composable.ts index 5520dd25b0..4612de5f09 100644 --- a/src/modules/data/board/BoardPageInformation.composable.ts +++ b/src/modules/data/board/BoardPageInformation.composable.ts @@ -4,6 +4,7 @@ import { createSharedComposable } from "@vueuse/core"; import { ref, Ref } from "vue"; import { useI18n } from "vue-i18n"; import { useBoardApi } from "./BoardApi.composable"; +import { BoardContextType } from "@/types/board/BoardContext"; const useBoardPageInformation = () => { const { t } = useI18n(); @@ -20,6 +21,7 @@ const useBoardPageInformation = () => { const pageTitle: Ref = ref(getPageTitle()); const roomId: Ref = ref(undefined); const breadcrumbs: Ref = ref([]); + const contextType: Ref = ref(); function getBreadcrumbs( contextInfo: { id: string; name: string } | undefined @@ -44,12 +46,14 @@ const useBoardPageInformation = () => { const contextInfo = await getContextInfo(id); pageTitle.value = getPageTitle(contextInfo?.name); breadcrumbs.value = getBreadcrumbs(contextInfo); + contextType.value = contextInfo?.type; roomId.value = contextInfo?.id; }; return { createPageInformation, breadcrumbs, + contextType, pageTitle, roomId, }; diff --git a/src/modules/data/board/BoardPageInformation.composable.unit.ts b/src/modules/data/board/BoardPageInformation.composable.unit.ts index fd85ba2585..fdfe1001d7 100644 --- a/src/modules/data/board/BoardPageInformation.composable.unit.ts +++ b/src/modules/data/board/BoardPageInformation.composable.unit.ts @@ -2,6 +2,7 @@ import { mountComposable } from "@@/tests/test-utils/mountComposable"; import { createMock, DeepMocked } from "@golevelup/ts-jest"; import { useBoardApi } from "./BoardApi.composable"; import { useSharedBoardPageInformation } from "./BoardPageInformation.composable"; +import { BoardContextType } from "@/types/board/BoardContext"; jest.mock("./BoardApi.composable"); const mockedUseBoardApi = jest.mocked(useBoardApi); @@ -36,13 +37,25 @@ describe("BoardPageInformation.composable", () => { const setup = () => { mockedBoardApiCalls.getContextInfo.mockResolvedValue({ id: "courseId", + type: BoardContextType.Course, name: "Course #1", }); - const { createPageInformation, breadcrumbs, pageTitle, roomId } = - mountComposable(() => useSharedBoardPageInformation()); - - return { createPageInformation, breadcrumbs, pageTitle, roomId }; + const { + createPageInformation, + breadcrumbs, + contextType, + pageTitle, + roomId, + } = mountComposable(() => useSharedBoardPageInformation()); + + return { + createPageInformation, + breadcrumbs, + contextType, + pageTitle, + roomId, + }; }; it("should return two breadcrumbs: 1. course page and and 2. course-overview page", async () => { @@ -74,6 +87,16 @@ describe("BoardPageInformation.composable", () => { expect(roomId.value).toEqual("courseId"); }); + + it("should set context type", async () => { + const { createPageInformation, contextType } = setup(); + + const fakeId = "abc123-2"; + + await createPageInformation(fakeId); + + expect(contextType.value).toEqual(BoardContextType.Course); + }); }); describe("when board context does not exist", () => { diff --git a/src/modules/data/room/RoomDetails.state.ts b/src/modules/data/room/RoomDetails.state.ts deleted file mode 100644 index 58ef05ca6c..0000000000 --- a/src/modules/data/room/RoomDetails.state.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Room } from "@/types/room/Room"; -import { delay } from "@/utils/helpers"; -import { ref } from "vue"; -import { roomsData } from "./rooms-mock-data"; - -export const useRoomDetailsState = () => { - const room = ref(); - const isLoading = ref(true); - const isRoom = ref(false); - - const fetchRoom = async (id: string) => { - await delay(100); - // TODO call API - room.value = roomsData.find((r) => r.id === id); - isRoom.value = room.value != null; - isLoading.value = false; - }; - - return { - fetchRoom, - isLoading, - isRoom, - room, - }; -}; diff --git a/src/modules/data/room/RoomDetails.store.ts b/src/modules/data/room/RoomDetails.store.ts new file mode 100644 index 0000000000..a0dd51c2ac --- /dev/null +++ b/src/modules/data/room/RoomDetails.store.ts @@ -0,0 +1,44 @@ +import { Room } from "@/types/room/Room"; +import { delay } from "@/utils/helpers"; +import { ref } from "vue"; +import { roomsData } from "./rooms-mock-data"; +import { defineStore } from "pinia"; + +export enum RoomVariant { + ROOM = "room", + COURSE_ROOM = "courseRoom", +} + +export const useRoomDetailsStore = defineStore("roomDetailsStore", () => { + const isLoading = ref(true); + const room = ref(); + const roomVariant = ref(); + + const fetchRoom = async (id: string) => { + await delay(100); + // TODO call API + room.value = roomsData.find((r) => r.id === id); + roomVariant.value = + room.value != null ? RoomVariant.ROOM : RoomVariant.COURSE_ROOM; + isLoading.value = false; + }; + + const resetState = () => { + isLoading.value = true; + room.value = undefined; + }; + + const deactivateRoom = () => { + resetState(); + isLoading.value = false; + }; + + return { + deactivateRoom, + fetchRoom, + isLoading, + resetState, + room, + roomVariant, + }; +}); diff --git a/src/modules/data/room/index.ts b/src/modules/data/room/index.ts index e86232bd40..c730814c25 100644 --- a/src/modules/data/room/index.ts +++ b/src/modules/data/room/index.ts @@ -1,5 +1,5 @@ export { useCourseApi } from "./courseApi.composable"; export { useRoomsState } from "./Rooms.state"; -export { useRoomDetailsState } from "./RoomDetails.state"; +export { useRoomDetailsStore, RoomVariant } from "./RoomDetails.store"; export { useCourseInfoApi } from "./courseInfoApi.composable"; export { useCourseList } from "./courseList.composable"; diff --git a/src/modules/feature/board-deleted-element/DeletedElement.unit.ts b/src/modules/feature/board-deleted-element/DeletedElement.unit.ts index b573e6c11d..cca12dbf06 100644 --- a/src/modules/feature/board-deleted-element/DeletedElement.unit.ts +++ b/src/modules/feature/board-deleted-element/DeletedElement.unit.ts @@ -1,5 +1,4 @@ -import { ContentElementType, DeletedElementResponse } from "@/serverApi/v3"; -import { timestampsResponseFactory } from "@@/tests/test-utils"; +import { deletedElementResponseFactory } from "@@/tests/test-utils"; import { createTestingI18n, createTestingVuetify, @@ -15,16 +14,6 @@ import DeletedElementMenu from "./DeletedElementMenu.vue"; jest.mock("@data-board"); -const DELETED_ELEMENT: DeletedElementResponse = { - id: "deleted-element-id", - content: { - deletedElementType: ContentElementType.ExternalTool, - title: "Deleted Tool", - }, - type: ContentElementType.Deleted, - timestamps: timestampsResponseFactory.build(), -}; - describe("DeletedElement", () => { let useBoardFocusHandlerMock: DeepMocked< ReturnType @@ -50,7 +39,7 @@ describe("DeletedElement", () => { const getWrapper = ( props: ComponentProps = { - element: DELETED_ELEMENT, + element: deletedElementResponseFactory.build(), isEditMode: false, } ) => { @@ -75,7 +64,7 @@ describe("DeletedElement", () => { useBoardPermissionsMock.isTeacher = false; const { wrapper } = getWrapper({ - element: DELETED_ELEMENT, + element: deletedElementResponseFactory.build(), isEditMode: true, }); @@ -97,7 +86,7 @@ describe("DeletedElement", () => { describe("when in edit mode", () => { const setup = () => { const { wrapper } = getWrapper({ - element: DELETED_ELEMENT, + element: deletedElementResponseFactory.build(), isEditMode: true, }); @@ -118,7 +107,7 @@ describe("DeletedElement", () => { describe("when not in edit mode", () => { const setup = () => { const { wrapper } = getWrapper({ - element: DELETED_ELEMENT, + element: deletedElementResponseFactory.build(), isEditMode: false, }); @@ -138,24 +127,26 @@ describe("DeletedElement", () => { describe("when deleting the element", () => { const setup = () => { + const deletedElement = deletedElementResponseFactory.build(); const { wrapper } = getWrapper({ - element: DELETED_ELEMENT, + element: deletedElement, isEditMode: true, }); return { wrapper, + deletedElement, }; }; it("should emit an event", async () => { - const { wrapper } = setup(); + const { wrapper, deletedElement } = setup(); wrapper.findComponent(DeletedElementMenu).vm.$emit("delete:element"); await nextTick(); expect(wrapper.emitted("delete:element")).toEqual([ - [DELETED_ELEMENT.id], + [deletedElement.id], ]); }); }); @@ -165,7 +156,7 @@ describe("DeletedElement", () => { describe("when the deleted element was an external tool element", () => { const setup = () => { const { wrapper } = getWrapper({ - element: DELETED_ELEMENT, + element: deletedElementResponseFactory.build(), isEditMode: true, }); diff --git a/src/modules/feature/board-external-tool-element/ExternalToolElement.unit.ts b/src/modules/feature/board-external-tool-element/ExternalToolElement.unit.ts index 147e2d4be3..f769c66845 100644 --- a/src/modules/feature/board-external-tool-element/ExternalToolElement.unit.ts +++ b/src/modules/feature/board-external-tool-element/ExternalToolElement.unit.ts @@ -1,8 +1,4 @@ -import { - ConfigResponse, - ContentElementType, - ExternalToolElementResponse, -} from "@/serverApi/v3"; +import { ConfigResponse, ExternalToolElementResponse } from "@/serverApi/v3"; import EnvConfigModule from "@/store/env-config"; import { BusinessError } from "@/store/types/commons"; import { ENV_CONFIG_MODULE_KEY } from "@/utils/inject"; @@ -11,8 +7,8 @@ import { contextExternalToolConfigurationStatusFactory, contextExternalToolFactory, externalToolDisplayDataFactory, + externalToolElementResponseFactory, schoolToolConfigurationStatusFactory, - timestampsResponseFactory, } from "@@/tests/test-utils"; import { createTestingI18n, @@ -39,15 +35,6 @@ jest.mock("@data-board"); jest.mock("@data-external-tool"); jest.mock("@util-board"); -const EMPTY_TEST_ELEMENT: ExternalToolElementResponse = { - id: "external-tool-element-id", - content: { - contextExternalToolId: null, - }, - type: ContentElementType.ExternalTool, - timestamps: timestampsResponseFactory.build(), -}; - describe("ExternalToolElement", () => { let useContentElementStateMock: DeepMocked< ReturnType @@ -154,10 +141,9 @@ describe("ExternalToolElement", () => { it("should load the display data", async () => { getWrapper( { - element: { - ...EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build({ content: { contextExternalToolId: "contextExternalToolId" }, - }, + }), isEditMode: false, }, externalToolDisplayDataFactory.build({ @@ -175,10 +161,9 @@ describe("ExternalToolElement", () => { it("should load the launch request", async () => { getWrapper( { - element: { - ...EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build({ content: { contextExternalToolId: "contextExternalToolId" }, - }, + }), isEditMode: false, }, externalToolDisplayDataFactory.build({ @@ -198,10 +183,9 @@ describe("ExternalToolElement", () => { it("should not load the launch request", async () => { getWrapper( { - element: { - ...EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build({ content: { contextExternalToolId: "contextExternalToolId" }, - }, + }), isEditMode: false, }, externalToolDisplayDataFactory.build({ @@ -224,10 +208,9 @@ describe("ExternalToolElement", () => { it("should not load the launch request", async () => { getWrapper( { - element: { - ...EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build({ content: { contextExternalToolId: "contextExternalToolId" }, - }, + }), isEditMode: false, }, externalToolDisplayDataFactory.build({ @@ -249,10 +232,9 @@ describe("ExternalToolElement", () => { it("should not load the launch request", async () => { getWrapper( { - element: { - ...EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build({ content: { contextExternalToolId: "contextExternalToolId" }, - }, + }), isEditMode: false, }, externalToolDisplayDataFactory.build({ @@ -274,10 +256,9 @@ describe("ExternalToolElement", () => { it("should not load the launch request", async () => { getWrapper( { - element: { - ...EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build({ content: { contextExternalToolId: "contextExternalToolId" }, - }, + }), isEditMode: false, }, externalToolDisplayDataFactory.build({ @@ -297,11 +278,11 @@ describe("ExternalToolElement", () => { describe("when the element does not have a tool attached", () => { it("should open the configuration dialog immediately", async () => { - useSharedLastCreatedElementMock.lastCreatedElementId.value = - EMPTY_TEST_ELEMENT.id; + const element = externalToolElementResponseFactory.build(); + useSharedLastCreatedElementMock.lastCreatedElementId.value = element.id; const { wrapper } = getWrapper({ - element: EMPTY_TEST_ELEMENT, + element, isEditMode: true, }); @@ -316,7 +297,7 @@ describe("ExternalToolElement", () => { it("should not load the display data", async () => { getWrapper({ - element: EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build(), isEditMode: false, }); @@ -329,7 +310,7 @@ describe("ExternalToolElement", () => { it("should not load the launch request", async () => { getWrapper({ - element: EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build(), isEditMode: false, }); @@ -346,7 +327,7 @@ describe("ExternalToolElement", () => { describe("when not in edit mode", () => { const setup = () => { const { wrapper } = getWrapper({ - element: EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build(), isEditMode: false, }); @@ -367,7 +348,7 @@ describe("ExternalToolElement", () => { describe("when in edit mode", () => { const setup = () => { const { wrapper } = getWrapper({ - element: EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build(), isEditMode: true, }); @@ -393,10 +374,9 @@ describe("ExternalToolElement", () => { const { wrapper } = getWrapper( { - element: { - ...EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build({ content: { contextExternalToolId }, - }, + }), isEditMode: false, }, externalToolDisplayDataFactory.build({ @@ -427,10 +407,9 @@ describe("ExternalToolElement", () => { const { wrapper } = getWrapper( { - element: { - ...EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build({ content: { contextExternalToolId }, - }, + }), isEditMode: false, }, externalToolDisplayDataFactory.build({ @@ -459,15 +438,12 @@ describe("ExternalToolElement", () => { describe("Loading", () => { describe("when the component is loading", () => { const setup = () => { - const contextExternalToolId = "context-external-tool-id"; - useExternalToolElementDisplayStateMock.isLoading = ref(true); const { wrapper } = getWrapper({ - element: { - ...EMPTY_TEST_ELEMENT, - content: { contextExternalToolId }, - }, + element: externalToolElementResponseFactory.build({ + content: { contextExternalToolId: "contextExternalToolId" }, + }), isEditMode: false, }); @@ -493,10 +469,9 @@ describe("ExternalToolElement", () => { const { wrapper } = getWrapper( { - element: { - ...EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build({ content: { contextExternalToolId }, - }, + }), isEditMode: false, }, externalToolDisplayDataFactory.build({ contextExternalToolId }) @@ -521,7 +496,7 @@ describe("ExternalToolElement", () => { describe("when clicking on a un-configured tool card in edit mode", () => { const setup = () => { const { wrapper } = getWrapper({ - element: EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build(), isEditMode: true, }); @@ -557,7 +532,7 @@ describe("ExternalToolElement", () => { ); const { wrapper } = getWrapper({ - element: EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build(), isEditMode: true, }); @@ -603,10 +578,9 @@ describe("ExternalToolElement", () => { describe("when clicking on a configured tool card", () => { const setup = () => { const { wrapper } = getWrapper({ - element: { - ...EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build({ content: { contextExternalToolId: "contextExternalToolId" }, - }, + }), isEditMode: false, }); @@ -657,7 +631,7 @@ describe("ExternalToolElement", () => { const { wrapper } = getWrapper( { - element: EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build(), isEditMode: true, }, externalToolDisplayDataFactory.build() @@ -689,7 +663,7 @@ describe("ExternalToolElement", () => { const { wrapper } = getWrapper( { - element: EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build(), isEditMode: true, }, externalToolDisplayDataFactory.build() @@ -718,7 +692,7 @@ describe("ExternalToolElement", () => { const { wrapper } = getWrapper( { - element: EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build(), isEditMode: true, }, externalToolDisplayDataFactory.build({ @@ -747,10 +721,9 @@ describe("ExternalToolElement", () => { jest.useFakeTimers({ legacyFakeTimers: true }); const { wrapper, refreshTime } = getWrapper( { - element: { - ...EMPTY_TEST_ELEMENT, + element: externalToolElementResponseFactory.build({ content: { contextExternalToolId: "contextExternalToolId" }, - }, + }), isEditMode: false, }, externalToolDisplayDataFactory.build({ @@ -780,4 +753,152 @@ describe("ExternalToolElement", () => { ).toHaveBeenCalledTimes(2); }); }); + + describe("when moving the element with arrow keys", () => { + const setup = () => { + const { wrapper } = getWrapper({ + element: externalToolElementResponseFactory.build({ + content: { contextExternalToolId: null }, + }), + isEditMode: true, + }); + + return { + wrapper, + }; + }; + + it("should emit an event", async () => { + const { wrapper } = setup(); + + const card = wrapper.getComponent({ ref: "externalToolElement" }); + await card.trigger("keydown.up"); + + expect(wrapper.emitted("move-keyboard:edit")).toHaveLength(1); + }); + }); + + describe("Aria label", () => { + describe("when no tool is selected", () => { + const setup = () => { + const { wrapper } = getWrapper({ + element: externalToolElementResponseFactory.build({ + content: { contextExternalToolId: null }, + }), + isEditMode: true, + }); + + return { + wrapper, + }; + }; + + it("should read that a tool needs to be selected", async () => { + const { wrapper } = setup(); + + const card = wrapper.getComponent({ ref: "externalToolElement" }); + + expect(card.attributes("aria-label")).toEqual( + "components.cardElement.externalToolElement, feature-board-external-tool-element.placeholder.selectTool" + ); + }); + }); + + describe("when a tool is displayed and will be opened in the same tab", () => { + const setup = () => { + const toolName = "testTool"; + const contextExternalToolId = "contextExternalToolId"; + + const { wrapper } = getWrapper( + { + element: externalToolElementResponseFactory.build({ + content: { contextExternalToolId }, + }), + isEditMode: true, + }, + externalToolDisplayDataFactory.build({ + name: toolName, + contextExternalToolId, + openInNewTab: false, + }) + ); + + return { + wrapper, + toolName, + }; + }; + + it("should read the tool name and the tab it is started in", async () => { + const { wrapper, toolName } = setup(); + + const card = wrapper.getComponent({ ref: "externalToolElement" }); + + expect(card.attributes("aria-label")).toEqual( + `components.cardElement.externalToolElement, ${toolName}, common.ariaLabel.sameTab` + ); + }); + }); + + describe("when a tool is displayed and will be opened in a new tab", () => { + const setup = () => { + const toolName = "testTool"; + const contextExternalToolId = "contextExternalToolId"; + + const { wrapper } = getWrapper( + { + element: externalToolElementResponseFactory.build({ + content: { contextExternalToolId }, + }), + isEditMode: true, + }, + externalToolDisplayDataFactory.build({ + name: toolName, + contextExternalToolId, + openInNewTab: true, + }) + ); + + return { + wrapper, + toolName, + }; + }; + + it("should read the tool name and the tab it is started in", async () => { + const { wrapper, toolName } = setup(); + + const card = wrapper.getComponent({ ref: "externalToolElement" }); + + expect(card.attributes("aria-label")).toEqual( + `components.cardElement.externalToolElement, ${toolName}, common.ariaLabel.newTab` + ); + }); + }); + + describe("when a tool is selected and currently loading", () => { + const setup = () => { + const { wrapper } = getWrapper({ + element: externalToolElementResponseFactory.build({ + content: { contextExternalToolId: "contextExternalToolId" }, + }), + isEditMode: true, + }); + + return { + wrapper, + }; + }; + + it("should read that the tool is loading", async () => { + const { wrapper } = setup(); + + const card = wrapper.getComponent({ ref: "externalToolElement" }); + + expect(card.attributes("aria-label")).toEqual( + `components.cardElement.externalToolElement, common.loading.text` + ); + }); + }); + }); }); diff --git a/src/modules/feature/board-external-tool-element/ExternalToolElement.vue b/src/modules/feature/board-external-tool-element/ExternalToolElement.vue index 1a2b67bb37..36912b7281 100644 --- a/src/modules/feature/board-external-tool-element/ExternalToolElement.vue +++ b/src/modules/feature/board-external-tool-element/ExternalToolElement.vue @@ -10,6 +10,7 @@ tabindex="0" role="button" :loading="isLoading" + :aria-label="ariaLabel" @keyup.enter="onClickElement" @keydown.up.down="onKeydownArrow" @keydown.stop @@ -55,7 +56,7 @@ - diff --git a/src/modules/feature/board/board/Board.unit.ts b/src/modules/feature/board/board/Board.unit.ts index de13d87f09..502c7ff497 100644 --- a/src/modules/feature/board/board/Board.unit.ts +++ b/src/modules/feature/board/board/Board.unit.ts @@ -132,6 +132,7 @@ describe("Board", () => { mockedUseSharedBoardPageInformation.mockReturnValue({ createPageInformation: jest.fn(), breadcrumbs: ref([]), + contextType: ref(), pageTitle: ref("page-title"), roomId: ref("room-id"), }); diff --git a/src/modules/feature/media-shelf/MediaBoardElementDisplay.vue b/src/modules/feature/media-shelf/MediaBoardElementDisplay.vue index 844dc9e25c..93cda800a9 100644 --- a/src/modules/feature/media-shelf/MediaBoardElementDisplay.vue +++ b/src/modules/feature/media-shelf/MediaBoardElementDisplay.vue @@ -18,20 +18,23 @@ :ripple="false" >
- - +
+ + +
+
@@ -70,6 +73,10 @@ defineProps({ element: { type: Object as PropType, }, + isUnavailable: { + type: Boolean, + default: false, + }, }); const card = ref(null); diff --git a/src/modules/feature/media-shelf/MediaBoardExternalToolDeletedElement.unit.ts b/src/modules/feature/media-shelf/MediaBoardExternalToolDeletedElement.unit.ts new file mode 100644 index 0000000000..24abee6797 --- /dev/null +++ b/src/modules/feature/media-shelf/MediaBoardExternalToolDeletedElement.unit.ts @@ -0,0 +1,104 @@ +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; +import { deletedElementResponseFactory } from "@@/tests/test-utils"; +import { mount } from "@vue/test-utils"; +import { BoardMenuActionDelete } from "@ui-board"; +import { nextTick } from "vue"; +import { ComponentProps } from "vue-component-type-helpers"; +import { VBtn } from "vuetify/lib/components/index.mjs"; +import MediaBoardExternalToolElementMenu from "./MediaBoardExternalToolElementMenu.vue"; +import MediaBoardDeletedElement from "./MediaBoardExternalToolDeletedElement.vue"; + +describe("MediaBoardDeletedElement", () => { + const getWrapper = ( + props: ComponentProps, + stubThreeDotMenu = true + ) => { + const wrapper = mount(MediaBoardDeletedElement, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + props, + stubs: { + MediaBoardExternalToolElementMenu: stubThreeDotMenu, + }, + }); + + return { + wrapper, + }; + }; + + describe("three dot menu", () => { + describe("when clicking on the the three dot menu", () => { + const setupOverlayDiv = () => { + const overlayDiv = document.createElement("div"); + overlayDiv.className = "v-overlay-container"; + document.body.append(); + }; + + const setup = () => { + const deletedElement = deletedElementResponseFactory.build(); + + const { wrapper } = getWrapper( + { + element: deletedElement, + }, + false + ); + + setupOverlayDiv(); + + return { + wrapper, + }; + }; + + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("should show the delete action", async () => { + const { wrapper } = setup(); + + const menuBtn = wrapper + .getComponent(MediaBoardExternalToolElementMenu) + .getComponent(VBtn); + await menuBtn.trigger("click"); + + const deleteAction = wrapper.findComponent(BoardMenuActionDelete); + + expect(deleteAction.exists()).toEqual(true); + }); + }); + + describe("when deleting the element from the menu", () => { + const setup = () => { + const deletedElement = deletedElementResponseFactory.build(); + + const { wrapper } = getWrapper({ + element: deletedElement, + }); + + return { + wrapper, + deletedElement, + }; + }; + + it("should emit a delete event", async () => { + const { wrapper, deletedElement } = setup(); + + const menu = wrapper.getComponent(MediaBoardExternalToolElementMenu); + menu.vm.$emit("delete:element"); + await nextTick(); + + expect(wrapper.emitted("delete:element")).toEqual([ + [deletedElement.id], + ]); + }); + }); + }); +}); diff --git a/src/modules/feature/media-shelf/MediaBoardExternalToolDeletedElement.vue b/src/modules/feature/media-shelf/MediaBoardExternalToolDeletedElement.vue new file mode 100644 index 0000000000..a89fdc3cff --- /dev/null +++ b/src/modules/feature/media-shelf/MediaBoardExternalToolDeletedElement.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/modules/feature/media-shelf/MediaBoardLine.unit.ts b/src/modules/feature/media-shelf/MediaBoardLine.unit.ts index 8072913789..8dc528e809 100644 --- a/src/modules/feature/media-shelf/MediaBoardLine.unit.ts +++ b/src/modules/feature/media-shelf/MediaBoardLine.unit.ts @@ -1,5 +1,9 @@ import { MediaBoardLayoutType } from "@/serverApi/v3"; -import { mediaLineResponseFactory } from "@@/tests/test-utils"; +import { + deletedElementResponseFactory, + mediaExternalToolElementResponseFactory, + mediaLineResponseFactory, +} from "@@/tests/test-utils"; import { createTestingI18n, createTestingVuetify, @@ -17,6 +21,7 @@ import MediaBoardExternalToolElement from "./MediaBoardExternalToolElement.vue"; import MediaBoardLine from "./MediaBoardLine.vue"; import MediaBoardLineHeader from "./MediaBoardLineHeader.vue"; import MediaBoardLineMenu from "./MediaBoardLineMenu.vue"; +import MediaBoardExternalToolDeletedElement from "./MediaBoardExternalToolDeletedElement.vue"; jest.mock("@vueuse/core", () => { return { @@ -518,4 +523,58 @@ describe("MediaBoardLine", () => { expect(wrapper.emitted("delete:element")).toEqual([["elementId"]]); }); }); + + describe("when rendering an element", () => { + describe("when the element response is a DeletedElementResponse", () => { + const setup = () => { + const { wrapper } = getWrapper({ + line: mediaLineResponseFactory.build({ + elements: deletedElementResponseFactory.buildList(1), + }), + layout: MediaBoardLayoutType.List, + index: 0, + }); + + return { + wrapper, + }; + }; + + it("should render the element as MediaBoardExternalToolDeletedElement", () => { + const { wrapper } = setup(); + + const deletedElement = wrapper.findComponent( + MediaBoardExternalToolDeletedElement + ); + + expect(deletedElement.exists()).toEqual(true); + }); + }); + + describe("when the element response is a MediaExternalToolElementResponse", () => { + const setup = () => { + const { wrapper } = getWrapper({ + line: mediaLineResponseFactory.build({ + elements: mediaExternalToolElementResponseFactory.buildList(1), + }), + layout: MediaBoardLayoutType.List, + index: 0, + }); + + return { + wrapper, + }; + }; + + it("should render the element as MediaBoardExternalToolElement", () => { + const { wrapper } = setup(); + + const externalToolElement = wrapper.findComponent( + MediaBoardExternalToolElement + ); + + expect(externalToolElement.exists()).toEqual(true); + }); + }); + }); }); diff --git a/src/modules/feature/media-shelf/MediaBoardLine.vue b/src/modules/feature/media-shelf/MediaBoardLine.vue index a02618d611..121cccae22 100644 --- a/src/modules/feature/media-shelf/MediaBoardLine.vue +++ b/src/modules/feature/media-shelf/MediaBoardLine.vue @@ -53,9 +53,15 @@ @end="onElementDragEnd" >