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 134f5ef0e2..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.", @@ -1464,7 +1466,7 @@ export default { "pages.content.index.search.placeholder": "Lern-Store durchsuchen", "pages.content.init_state.img_alt": "Initial state Image", "pages.content.init_state.message": - "Hier findest du hochwertige, auf dein Bundesland abgestimmte Inhalte.
Unser Team erschließt fortlaufend neue Materialen, um so dein Lernerlebnis weiter zu verbessern.

Hinweis:

Die im Lernstore angezeigten Materialien liegen nicht auf unserem Server, sondern werden über Schnittstellen zu anderen Servern verfügbar gemacht (Quellen sind zum Beispiel einzelne Bildungsserver, WirLernenOnline, Mundo u.a.).
Aus diesem Grund hat unser Team auch keinen Einfluss auf die dauerhafte Verfügbarkeit einzelner Materialien und auf das vollumfängliche Materialangebot der einzelnen Quellen.

Im Rahmen der Nutzung in Bildungseinrichtungen ist das Kopieren der Online-Medien auf Speichermedien, auf ein privates Endgerät oder auf Lernplattformen für einen geschlossenen Benutzerkreis ggf. erlaubt, soweit dies für die interne Verteilung und/oder Nutzung erforderlich ist.
Nach Beendigung der Arbeiten mit den jeweiligen Online-Medien sind diese von den privaten Endgeräten, Datenträgern und Lernplattformen zu löschen; spätestens beim Verlassen der Bildungseinrichtung.
Eine grundsätzliche Veröffentlichung (z.B. im Internet) der Online-Medien oder mit Teilen daraus neu hergestellten neuen und/oder bearbeiteten Werken ist grundsätzlich nicht zulässig, bzw. bedarf der Zustimmung des Rechtegebers.", + "Hier findest du hochwertige, auf dein Bundesland abgestimmte Inhalte.
Unser Team erschließt fortlaufend neue Materialien, um so dein Lernerlebnis weiter zu verbessern.

Hinweis:

Die im Lernstore angezeigten Materialien liegen nicht auf unserem Server, sondern werden über Schnittstellen zu anderen Servern verfügbar gemacht (Quellen sind zum Beispiel einzelne Bildungsserver, WirLernenOnline, Mundo u.a.).
Aus diesem Grund hat unser Team auch keinen Einfluss auf die dauerhafte Verfügbarkeit einzelner Materialien und auf das vollumfängliche Materialangebot der einzelnen Quellen.

Im Rahmen der Nutzung in Bildungseinrichtungen ist das Kopieren der Online-Medien auf Speichermedien, auf ein privates Endgerät oder auf Lernplattformen für einen geschlossenen Benutzerkreis ggf. erlaubt, soweit dies für die interne Verteilung und/oder Nutzung erforderlich ist.
Nach Beendigung der Arbeiten mit den jeweiligen Online-Medien sind diese von den privaten Endgeräten, Datenträgern und Lernplattformen zu löschen; spätestens beim Verlassen der Bildungseinrichtung.
Eine grundsätzliche Veröffentlichung (z.B. im Internet) der Online-Medien oder mit Teilen daraus neu hergestellten neuen und/oder bearbeiteten Werken ist grundsätzlich nicht zulässig, bzw. bedarf der Zustimmung des Rechtegebers.", "pages.content.init_state.title": "Willkommen im Lern-Store!", "pages.content.label.chooseACourse": "Wähle einen Kurs / Fach", "pages.content.label.chooseALessonTopic": "Wähle ein Unterrichtsthema", 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 05ee7cd409..8a93c14a6c 100644 --- a/src/modules/feature/board/board/Board.unit.ts +++ b/src/modules/feature/board/board/Board.unit.ts @@ -129,6 +129,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 ac7daacce9..a30a07362a 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, @@ -18,6 +22,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 { @@ -519,4 +524,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 32bbe7526e..ba3165ee9a 100644 --- a/src/modules/feature/media-shelf/MediaBoardLine.vue +++ b/src/modules/feature/media-shelf/MediaBoardLine.vue @@ -53,9 +53,15 @@ @end="onElementDragEnd" >