diff --git a/src/assets/img/collaborativeEditor.svg b/src/assets/img/collaborativeEditor.svg new file mode 100644 index 0000000000..62e9ae705f --- /dev/null +++ b/src/assets/img/collaborativeEditor.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/index.d.ts b/src/assets/img/index.d.ts index 1294355bfd..c391661c57 100644 --- a/src/assets/img/index.d.ts +++ b/src/assets/img/index.d.ts @@ -6,3 +6,8 @@ declare module "@/assets/img/image-not-available.svg" { const value: string; export default value; } + +declare module "@/assets/img/collaborativeEditor.svg" { + const value: string; + export default value; +} diff --git a/src/locales/de.ts b/src/locales/de.ts index 925c14d99a..ffdda2d4ce 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -313,6 +313,7 @@ export default { "components.cardElement.deleteElement": "löschen", "components.cardElement.dragElement": "verschieben", "components.cardElement.drawingElement": "Whiteboard", + "components.cardElement.collaborativeTextEditorElement": "Texteditor", "components.cardElement.fileElement.altDescription": "Eine kurze Beschreibung hilft Personen, die das Bild nicht sehen können.", "components.cardElement.fileElement.alternativeText": "Alternativtext", @@ -367,6 +368,8 @@ export default { "components.elementTypeSelection.elements.externalToolElement.subtitle": "Externe Tools", "components.elementTypeSelection.elements.fileElement.subtitle": "Datei", + "components.elementTypeSelection.elements.collaborativeTextEditor.subtitle": + "Texteditor", "components.elementTypeSelection.elements.linkElement.subtitle": "Link", "components.elementTypeSelection.elements.submissionElement.subtitle": "Abgabe", diff --git a/src/locales/en.ts b/src/locales/en.ts index 1384ef89ea..65e86ceeb8 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -310,6 +310,7 @@ export default { "components.cardElement.deleteElement": "Delete element", "components.cardElement.dragElement": "Move element", "components.cardElement.drawingElement": "Whiteboard", + "components.cardElement.collaborativeTextEditorElement": "Texteditor", "components.cardElement.fileElement.altDescription": "A short description helps people who cannot see the picture.", "components.cardElement.fileElement.alternativeText": "Alternative Text", @@ -363,6 +364,8 @@ export default { "components.elementTypeSelection.elements.externalToolElement.subtitle": "External tools", "components.elementTypeSelection.elements.fileElement.subtitle": "File", + "components.elementTypeSelection.elements.collaborativeTextEditor.subtitle": + "Texteditor", "components.elementTypeSelection.elements.linkElement.subtitle": "Link", "components.elementTypeSelection.elements.submissionElement.subtitle": "Submission", diff --git a/src/locales/es.ts b/src/locales/es.ts index c61c9ad4f3..fc1ce01fc2 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -316,6 +316,7 @@ export default { "components.cardElement.deleteElement": "Suprimir elemento", "components.cardElement.dragElement": "Mover elemento", "components.cardElement.drawingElement": "Pizarra", + "components.cardElement.collaborativeTextEditorElement": "Texteditor", "components.cardElement.fileElement.altDescription": "Una breve descripción ayuda a las personas que no pueden ver la imagen.", "components.cardElement.fileElement.alternativeText": "Texto alternativo", @@ -369,7 +370,10 @@ export default { "components.elementTypeSelection.dialog.title": "Añadir elemento", "components.elementTypeSelection.elements.externalToolElement.subtitle": "Herramientas externas", - "components.elementTypeSelection.elements.fileElement.subtitle": "Archivo", + "components.elementTypeSelection.elements.fileElement.subtitle": + "Editor de texto", + "components.elementTypeSelection.elements.collaborativeTextEditor.subtitle": + "Text Editor", "components.elementTypeSelection.elements.linkElement.subtitle": "Enlace", "components.elementTypeSelection.elements.submissionElement.subtitle": "Envíos", diff --git a/src/locales/uk.ts b/src/locales/uk.ts index a1a2f54450..1a9f04ae3f 100644 --- a/src/locales/uk.ts +++ b/src/locales/uk.ts @@ -317,6 +317,7 @@ export default { "components.cardElement.deleteElement": "Видалити елемент", "components.cardElement.dragElement": "Перемістити елемент", "components.cardElement.drawingElement": "Дошка", + "components.cardElement.collaborativeTextEditorElement": "Texteditor", "components.cardElement.fileElement.altDescription": "Короткий опис допомагає людям, які не бачать зображення.", "components.cardElement.fileElement.alternativeText": "альтернативний текст", @@ -370,6 +371,8 @@ export default { "components.elementTypeSelection.elements.externalToolElement.subtitle": "Зовнішні інструменти", "components.elementTypeSelection.elements.fileElement.subtitle": "Файл", + "components.elementTypeSelection.elements.collaborativeTextEditor.subtitle": + "Текстовий редактор", "components.elementTypeSelection.elements.linkElement.subtitle": "Посилання", "components.elementTypeSelection.elements.submissionElement.subtitle": "Подання", diff --git a/src/modules/feature/board-collaborative-text-editor-element/CollaborativeTextEditorElement.unit.ts b/src/modules/feature/board-collaborative-text-editor-element/CollaborativeTextEditorElement.unit.ts new file mode 100644 index 0000000000..bd21177ca2 --- /dev/null +++ b/src/modules/feature/board-collaborative-text-editor-element/CollaborativeTextEditorElement.unit.ts @@ -0,0 +1,217 @@ +import { CollaborativeTextEditorParentType } from "@/serverApi/v3"; +import NotifierModule from "@/store/notifier"; +import { NOTIFIER_MODULE_KEY } from "@/utils/inject"; +import { createModuleMocks } from "@/utils/mock-store-module"; +import { collaborativeTextEditorElementResponseFactory } from "@@/tests/test-utils"; +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; +import { CollaborativeTextEditorElement } from "@feature-board-collaborative-text-editor-element"; +import { createMock } from "@golevelup/ts-jest"; +import { ContentElementBar } from "@ui-board"; +import { mount } from "@vue/test-utils"; +import { nextTick } from "vue"; +import CollaborativeTextEditorElementMenu from "./components/CollaborativeTextEditorElementMenu.vue"; +import { setupCollaborativeTextEditorApiMock } from "./test-utils/collaborativeTextEditorApiMock"; + +// Mocks +jest.mock("@data-board", () => ({ + useBoardFocusHandler: jest.fn(), + useContentElementState: jest.fn(() => ({ modelValue: {} })), + useDeleteConfirmationDialog: jest.fn(), +})); +jest.mock("@feature-board"); +jest.mock("./composables/CollaborativeTextEditorApi.composable"); + +describe("CollaborativeTextEditorElement", () => { + const notifierModule = createModuleMocks(NotifierModule); + + const setup = (props: { isEditMode: boolean; getUrlHasError?: boolean }) => { + document.body.setAttribute("data-app", "true"); + + const element = collaborativeTextEditorElementResponseFactory.build(); + + const resolvedValue = props.getUrlHasError + ? undefined + : `${CollaborativeTextEditorParentType.ContentElement}/${element.id}`; + const getUrlMock = jest.fn().mockResolvedValueOnce(resolvedValue); + + const { getUrl } = setupCollaborativeTextEditorApiMock({ + getUrlMock, + }); + + const wrapper = mount(CollaborativeTextEditorElement, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + provide: { + [NOTIFIER_MODULE_KEY.valueOf()]: notifierModule, + }, + }, + propsData: { ...props, element: element }, + }); + + const windowMock = createMock(); + jest.spyOn(window, "open").mockImplementation(() => windowMock); + + return { + wrapper, + isEditMode: true, + element: element, + getUrl, + windowMock, + }; + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("when component is not in edit-mode", () => { + it("should render the element corrrectly", () => { + const { wrapper } = setup({ + isEditMode: false, + }); + expect( + wrapper.findComponent(CollaborativeTextEditorElement).isVisible() + ).toBe(true); + }); + + it("should render the ContentElementBar", () => { + const { wrapper } = setup({ + isEditMode: false, + }); + expect(wrapper.findComponent(ContentElementBar).exists()).toBe(true); + }); + + it("should not render the BoardMenu", () => { + const { wrapper } = setup({ + isEditMode: false, + }); + + const boardMenu = wrapper.findComponent( + CollaborativeTextEditorElementMenu + ); + + expect(boardMenu.exists()).toBe(false); + }); + + describe("when element is clicked", () => { + describe("when getUrl returns successful", () => { + it("should call getUrl", async () => { + const { wrapper, element, getUrl } = setup({ + isEditMode: false, + }); + + const card = wrapper.findComponent({ + ref: "collaborativetextEditorElement", + }); + + await card.trigger("click"); + + expect(getUrl).toHaveBeenCalledTimes(1); + expect(getUrl).toHaveBeenCalledWith( + element.id, + CollaborativeTextEditorParentType.ContentElement + ); + }); + + it("should open collaborative text editor in new tab", async () => { + const { wrapper, element, windowMock } = setup({ + isEditMode: false, + }); + + const card = wrapper.findComponent({ + ref: "collaborativetextEditorElement", + }); + + await card.trigger("click"); + + expect(window.open).toHaveBeenCalledTimes(1); + expect(windowMock.location).toBe( + `${CollaborativeTextEditorParentType.ContentElement}/${element.id}` + ); + }); + }); + + describe("when getUrl returns undefined", () => { + it("should not open new tab", async () => { + const { wrapper, windowMock, element } = setup({ + isEditMode: false, + getUrlHasError: true, + }); + + const card = wrapper.findComponent({ + ref: "collaborativetextEditorElement", + }); + + await card.trigger("click"); + + expect(window.open).toHaveBeenCalledTimes(1); + expect(windowMock.location).not.toBe( + `${CollaborativeTextEditorParentType.ContentElement}/${element.id}` + ); + }); + }); + }); + }); + + describe("when component is in edit-mode", () => { + it("should render BoardMenu element", async () => { + const { wrapper } = setup({ + isEditMode: true, + }); + + const boardMenu = wrapper.findComponent( + CollaborativeTextEditorElementMenu + ); + expect(boardMenu.exists()).toBe(true); + }); + + describe("when move down is emitted by CollaborativeTextEditorElementMenu", () => { + it('should emit "move-down:edit" collaborative text editor', async () => { + const { wrapper } = setup({ + isEditMode: true, + }); + + const boardMenu = wrapper.findComponent( + CollaborativeTextEditorElementMenu + ); + boardMenu.vm.$emit("move-down:element"); + + expect(wrapper.emitted("move-down:edit")).toBeTruthy(); + }); + }); + + describe("when move up is clicked", () => { + it('should emit "move-up" collaborative text editor', async () => { + const { wrapper } = setup({ + isEditMode: true, + }); + + const boardMenu = wrapper.findComponent( + CollaborativeTextEditorElementMenu + ); + boardMenu.vm.$emit("move-up:element"); + + expect(wrapper.emitted("move-up:edit")).toBeTruthy(); + }); + }); + + describe("when delete is clicked", () => { + it('should emit "delete" collaborative text editor', async () => { + const { wrapper } = setup({ + isEditMode: true, + }); + + const boardMenu = wrapper.findComponent( + CollaborativeTextEditorElementMenu + ); + boardMenu.vm.$emit("delete:element", Promise.resolve(true)); + await nextTick(); + + expect(wrapper.emitted("delete:element")).toBeTruthy(); + }); + }); + }); +}); diff --git a/src/modules/feature/board-collaborative-text-editor-element/CollaborativeTextEditorElement.vue b/src/modules/feature/board-collaborative-text-editor-element/CollaborativeTextEditorElement.vue new file mode 100644 index 0000000000..8850eaec26 --- /dev/null +++ b/src/modules/feature/board-collaborative-text-editor-element/CollaborativeTextEditorElement.vue @@ -0,0 +1,100 @@ + + + diff --git a/src/modules/feature/board-collaborative-text-editor-element/components/CollaborativeTextEditorElementMenu.unit.ts b/src/modules/feature/board-collaborative-text-editor-element/components/CollaborativeTextEditorElementMenu.unit.ts new file mode 100644 index 0000000000..62f6daf21d --- /dev/null +++ b/src/modules/feature/board-collaborative-text-editor-element/components/CollaborativeTextEditorElementMenu.unit.ts @@ -0,0 +1,129 @@ +import setupDeleteConfirmationComposableMock from "@@/tests/test-utils/composable-mocks/setupDeleteConfirmationComposableMock"; +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; +import { + BoardMenuActionDelete, + BoardMenuActionMoveDown, + BoardMenuActionMoveUp, +} from "@ui-board"; +import { useDeleteConfirmationDialog } from "@ui-confirmation-dialog"; +import { shallowMount } from "@vue/test-utils"; +import { ref } from "vue"; +import CollaborativeTextEditorElementMenu from "./CollaborativeTextEditorElementMenu.vue"; + +// Mocks +jest.mock("@ui-confirmation-dialog"); + +const mockedUseDeleteConfirmationDialog = jest.mocked( + useDeleteConfirmationDialog +); + +describe("CollaborativeTextEditorElementMenu", () => { + const getWrapper = (propsData: { + isFirstElement: boolean; + isLastElement: boolean; + hasMultipleElements: boolean; + }) => { + document.body.setAttribute("data-app", "true"); + + const askDeleteConfirmationMock = async () => await Promise.resolve(true); + + setupDeleteConfirmationComposableMock({ + askDeleteConfirmationMock, + }); + + mockedUseDeleteConfirmationDialog.mockReturnValue({ + askDeleteConfirmation: askDeleteConfirmationMock, + isDeleteDialogOpen: ref(false), + }); + + const wrapper = shallowMount(CollaborativeTextEditorElementMenu, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + props: propsData, + }); + + return { + wrapper, + }; + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + const setup = () => { + const { wrapper } = getWrapper({ + hasMultipleElements: true, + isFirstElement: false, + isLastElement: false, + }); + + return { + wrapper, + }; + }; + + describe("Move Up Button", () => { + it("should have a menu option to move element up", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.findComponent(BoardMenuActionMoveUp); + + expect(menuItem.exists()).toEqual(true); + }); + + it("should emit the move-up event on click", async () => { + const { wrapper } = setup(); + + const menuItem = wrapper.findComponent(BoardMenuActionMoveUp); + + await menuItem.trigger("click"); + + expect(wrapper.emitted("move-up:element")).toBeDefined(); + }); + }); + + describe("Move Down Button", () => { + it("should have a menu option to move element down", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.findComponent(BoardMenuActionMoveDown); + + expect(menuItem.exists()).toEqual(true); + }); + + it("should emit the move-down event on click", async () => { + const { wrapper } = setup(); + + const menuItem = wrapper.findComponent(BoardMenuActionMoveDown); + + await menuItem.trigger("click"); + + expect(wrapper.emitted("move-down:element")).toBeDefined(); + }); + }); + + describe("Delete Button", () => { + it("should have a menu option to delete", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.findComponent(BoardMenuActionDelete); + + expect(menuItem.exists()).toEqual(true); + }); + + it("should emit the delete event on click", async () => { + const { wrapper } = setup(); + + const menuItem = wrapper.findComponent(BoardMenuActionDelete); + + await menuItem.trigger("click"); + + expect(wrapper.emitted("delete:element")).toBeDefined(); + }); + }); +}); diff --git a/src/modules/feature/board-collaborative-text-editor-element/components/CollaborativeTextEditorElementMenu.vue b/src/modules/feature/board-collaborative-text-editor-element/components/CollaborativeTextEditorElementMenu.vue new file mode 100644 index 0000000000..f0f36e250b --- /dev/null +++ b/src/modules/feature/board-collaborative-text-editor-element/components/CollaborativeTextEditorElementMenu.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/modules/feature/board-collaborative-text-editor-element/composables/CollaborativeTextEditorApi.composable.ts b/src/modules/feature/board-collaborative-text-editor-element/composables/CollaborativeTextEditorApi.composable.ts new file mode 100644 index 0000000000..3691642fc0 --- /dev/null +++ b/src/modules/feature/board-collaborative-text-editor-element/composables/CollaborativeTextEditorApi.composable.ts @@ -0,0 +1,67 @@ +import { + CollaborativeTextEditorApiFactory, + CollaborativeTextEditorApiInterface, + CollaborativeTextEditorParentType, +} from "@/serverApi/v3"; +import { $axios, mapAxiosErrorToResponseError } from "@/utils/api"; +import { createTestableGlobaleState } from "@/utils/create-global-state"; +import { useCollaborativeTextEditorNotifier } from "./CollaborativeTextEditorNotifications.composable"; + +export enum ErrorType { + Unauthorized = "Unauthorized", + Forbidden = "Forbidden", +} + +const collaborativeTextEditorApi = () => { + const collaborativeTextEditorApi: CollaborativeTextEditorApiInterface = + CollaborativeTextEditorApiFactory(undefined, "/v3", $axios); + + const { showForbiddenError, showUnauthorizedError, showInternalServerError } = + useCollaborativeTextEditorNotifier(); + + const getUrl = async ( + parentId: string, + parentType: CollaborativeTextEditorParentType + ): Promise => { + try { + const response = + await collaborativeTextEditorApi.collaborativeTextEditorControllerGetOrCreateCollaborativeTextEditorForParent( + parentId, + parentType + ); + + return response.data.url; + } catch (error) { + showError(error); + } + }; + + const showError = (error: unknown) => { + const responseError = mapAxiosErrorToResponseError(error); + const { message } = responseError; + + showMessageByType(message); + }; + + const showMessageByType = (message: ErrorType | string) => { + switch (message) { + case ErrorType.Unauthorized: + showUnauthorizedError(); + break; + case ErrorType.Forbidden: + showForbiddenError(); + break; + default: + showInternalServerError(); + break; + } + }; + + return { + getUrl, + }; +}; + +export const useCollaborativeTextEditorApi = createTestableGlobaleState( + collaborativeTextEditorApi +); diff --git a/src/modules/feature/board-collaborative-text-editor-element/composables/CollaborativeTextEditorApi.composable.unit.ts b/src/modules/feature/board-collaborative-text-editor-element/composables/CollaborativeTextEditorApi.composable.unit.ts new file mode 100644 index 0000000000..c36d589069 --- /dev/null +++ b/src/modules/feature/board-collaborative-text-editor-element/composables/CollaborativeTextEditorApi.composable.unit.ts @@ -0,0 +1,177 @@ +import * as serverApi from "@/serverApi/v3/api"; +import { mapAxiosErrorToResponseError } from "@/utils/api"; +import { + apiResponseErrorFactory, + axiosErrorFactory, +} from "@@/tests/test-utils"; +import { ObjectIdMock } from "@@/tests/test-utils/ObjectIdMock"; +import { createMock } from "@golevelup/ts-jest"; +import { AxiosResponse } from "axios"; +import { setupCollaborativeTextEditorNotifier } from "../test-utils/collaborativeTextEditorNotifier"; +import { + ErrorType, + useCollaborativeTextEditorApi, +} from "./CollaborativeTextEditorApi.composable"; + +jest.mock("./CollaborativeTextEditorNotifications.composable"); + +jest.mock("@/utils/api"); +const mockedMapAxiosErrorToResponseError = jest.mocked( + mapAxiosErrorToResponseError +); + +jest.mock( + "@/utils/create-global-state", + () => ({ + createTestableGlobaleState: (composable) => composable, + }) +); + +const setupErrorResponse = (message = "NOT_FOUND", code = 404) => { + const expectedPayload = apiResponseErrorFactory.build({ + message, + code, + }); + const responseError = axiosErrorFactory.build({ + response: { data: expectedPayload }, + }); + + return { + responseError, + expectedPayload, + }; +}; + +describe("CollaborativeTextEditorApi Composable", () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("getUrl", () => { + describe("when collaborativeTextEditorControllerGetOrCreateCollaborativeTextEditorForParent returns successful", () => { + const setup = () => { + const parentId = ObjectIdMock(); + const parentType = + serverApi.CollaborativeTextEditorParentType.ContentElement; + + const response = createMock< + AxiosResponse + >({ + data: { url: `${parentType}/${parentId}` }, + }); + + const collaborativeTextEditorApi = + createMock(); + jest + .spyOn(serverApi, "CollaborativeTextEditorApiFactory") + .mockReturnValue(collaborativeTextEditorApi); + collaborativeTextEditorApi.collaborativeTextEditorControllerGetOrCreateCollaborativeTextEditorForParent.mockResolvedValueOnce( + response + ); + + setupCollaborativeTextEditorNotifier(); + + return { + parentId, + parentType, + response, + collaborativeTextEditorApi, + }; + }; + + it("should call collaborativeTextEditorApi.collaborativeTextEditorControllerGetOrCreateCollaborativeTextEditorForParent", async () => { + const { parentId, parentType, collaborativeTextEditorApi } = setup(); + const { getUrl } = useCollaborativeTextEditorApi(); + + await getUrl(parentId, parentType); + + expect( + collaborativeTextEditorApi.collaborativeTextEditorControllerGetOrCreateCollaborativeTextEditorForParent + ).toHaveBeenCalledTimes(1); + expect( + collaborativeTextEditorApi.collaborativeTextEditorControllerGetOrCreateCollaborativeTextEditorForParent + ).toHaveBeenCalledWith(parentId, parentType); + }); + + it("should return url", async () => { + const { parentId, parentType, response } = setup(); + const { getUrl } = useCollaborativeTextEditorApi(); + + const url = await getUrl(parentId, parentType); + + expect(url).toEqual(response.data.url); + }); + }); + + describe("when collaborativeTextEditorControllerGetOrCreateCollaborativeTextEditorForParent returns error", () => { + const setup = (message?: string) => { + const parentId = ObjectIdMock(); + const parentType = + serverApi.CollaborativeTextEditorParentType.ContentElement; + + const { responseError, expectedPayload } = setupErrorResponse(message); + mockedMapAxiosErrorToResponseError.mockReturnValueOnce(expectedPayload); + + const collaborativeTextEditorApi = + createMock(); + jest + .spyOn(serverApi, "CollaborativeTextEditorApiFactory") + .mockReturnValue(collaborativeTextEditorApi); + collaborativeTextEditorApi.collaborativeTextEditorControllerGetOrCreateCollaborativeTextEditorForParent.mockRejectedValue( + responseError + ); + + const { + showInternalServerError, + showUnauthorizedError, + showForbiddenError, + } = setupCollaborativeTextEditorNotifier(); + + return { + parentId, + parentType, + showInternalServerError, + showUnauthorizedError, + showForbiddenError, + }; + }; + + it("should call showUnauthorizedError and pass error", async () => { + const { parentId, parentType, showUnauthorizedError } = setup( + ErrorType.Unauthorized + ); + + const { getUrl } = useCollaborativeTextEditorApi(); + + const result = await getUrl(parentId, parentType); + + expect(result).toBeUndefined(); + expect(showUnauthorizedError).toBeCalledTimes(1); + }); + + it("should call showForbiddenError and pass error", async () => { + const { parentId, parentType, showForbiddenError } = setup( + ErrorType.Forbidden + ); + + const { getUrl } = useCollaborativeTextEditorApi(); + + const result = await getUrl(parentId, parentType); + + expect(result).toBeUndefined(); + expect(showForbiddenError).toBeCalledTimes(1); + }); + + it("should call showInternalServerError and pass error", async () => { + const { parentId, parentType, showInternalServerError } = setup(); + + const { getUrl } = useCollaborativeTextEditorApi(); + + const result = await getUrl(parentId, parentType); + + expect(result).toBeUndefined(); + expect(showInternalServerError).toBeCalledTimes(1); + }); + }); + }); +}); diff --git a/src/modules/feature/board-collaborative-text-editor-element/composables/CollaborativeTextEditorNotifications.composable.ts b/src/modules/feature/board-collaborative-text-editor-element/composables/CollaborativeTextEditorNotifications.composable.ts new file mode 100644 index 0000000000..fe3c64e0cd --- /dev/null +++ b/src/modules/feature/board-collaborative-text-editor-element/composables/CollaborativeTextEditorNotifications.composable.ts @@ -0,0 +1,41 @@ +import { injectStrict, NOTIFIER_MODULE_KEY } from "@/utils/inject"; +import { useI18n } from "vue-i18n"; + +export const useCollaborativeTextEditorNotifier = () => { + const { t } = useI18n(); + const notifierModule = injectStrict(NOTIFIER_MODULE_KEY); + + const showFailure = (text: string | undefined) => { + notifierModule.show({ + text, + status: "error", + timeout: 5000, + }); + }; + + const showForbiddenError = () => { + const message = t("error.403"); + + showFailure(message); + }; + + const showUnauthorizedError = () => { + const message = t("error.401"); + + showFailure(message); + }; + + const showInternalServerError = () => { + const message = t("components.board.notifications.errors.notCreated", { + type: t("components.cardElement.collaborativeTextEditorElement"), + }); + + showFailure(message); + }; + + return { + showForbiddenError, + showUnauthorizedError, + showInternalServerError, + }; +}; diff --git a/src/modules/feature/board-collaborative-text-editor-element/composables/CollaborativeTextEditorNotifications.composable.unit.ts b/src/modules/feature/board-collaborative-text-editor-element/composables/CollaborativeTextEditorNotifications.composable.unit.ts new file mode 100644 index 0000000000..fab8bf3616 --- /dev/null +++ b/src/modules/feature/board-collaborative-text-editor-element/composables/CollaborativeTextEditorNotifications.composable.unit.ts @@ -0,0 +1,134 @@ +import NotifierModule from "@/store/notifier"; +import { NOTIFIER_MODULE_KEY } from "@/utils/inject"; +import { createModuleMocks } from "@/utils/mock-store-module"; +import { mountComposable } from "@@/tests/test-utils/mountComposable"; +import { useI18n } from "vue-i18n"; +import { useCollaborativeTextEditorNotifier } from "./CollaborativeTextEditorNotifications.composable"; + +jest.mock("vue-i18n", () => { + return { + ...jest.requireActual("vue-i18n"), + useI18n: jest.fn().mockReturnValue({ + t: jest + .fn() + .mockImplementation( + (key: string, dynamic?: object): string => + key + (dynamic ? ` ${JSON.stringify(dynamic)}` : "") + ), + n: jest.fn().mockImplementation((key: string) => key), + }), + }; +}); + +const mockI18nModule = jest.mocked(useI18n()); + +const notifierModule = createModuleMocks(NotifierModule); + +const setupMountComposable = () => { + return mountComposable(() => useCollaborativeTextEditorNotifier(), { + global: { + provide: { + [NOTIFIER_MODULE_KEY as symbol]: notifierModule, + }, + }, + }); +}; + +describe("CollaborativeTextEditorNotifications.composable", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("when showForbiddenError called", () => { + const setup = () => { + const i18nKey = "error.403"; + + return { i18nKey }; + }; + + it("should call i18n.t with correct props", () => { + const { showForbiddenError } = setupMountComposable(); + const { i18nKey } = setup(); + + showForbiddenError(); + + expect(mockI18nModule.t).toBeCalledWith(i18nKey); + }); + + it("should call showFailure with correct props", () => { + const { showForbiddenError } = setupMountComposable(); + const { i18nKey } = setup(); + + showForbiddenError(); + + expect(notifierModule.show).toBeCalledWith({ + status: "error", + text: i18nKey, + timeout: 5000, + }); + }); + }); + + describe("when showUnauthorizedError called", () => { + const setup = () => { + const i18nKey = "error.401"; + + return { i18nKey }; + }; + + it("should call i18n.t with correct props", () => { + const { showUnauthorizedError } = setupMountComposable(); + const { i18nKey } = setup(); + + showUnauthorizedError(); + + expect(mockI18nModule.t).toBeCalledWith(i18nKey); + }); + + it("should call showFailure with correct props", () => { + const { showUnauthorizedError } = setupMountComposable(); + const { i18nKey } = setup(); + + showUnauthorizedError(); + + expect(notifierModule.show).toBeCalledWith({ + status: "error", + text: i18nKey, + timeout: 5000, + }); + }); + }); + + describe("when showInternalServerError called", () => { + const setup = () => { + const i18nKey = "components.board.notifications.errors.notCreated"; + const type = "components.cardElement.collaborativeTextEditorElement"; + + return { i18nKey, type }; + }; + + it("should call i18n.t with correct props", () => { + const { showInternalServerError } = setupMountComposable(); + const { i18nKey, type } = setup(); + + showInternalServerError(); + + expect(mockI18nModule.t).toBeCalledWith(i18nKey, { type }); + }); + + it("should call showFailure with correct props", () => { + const { showInternalServerError } = setupMountComposable(); + const { i18nKey, type } = setup(); + + const text = i18nKey + ` ${JSON.stringify({ type })}`; + + showInternalServerError(); + + expect(notifierModule.show).toBeCalledWith({ + status: "error", + text, + timeout: 5000, + }); + }); + }); +}); diff --git a/src/modules/feature/board-collaborative-text-editor-element/index.ts b/src/modules/feature/board-collaborative-text-editor-element/index.ts new file mode 100644 index 0000000000..0c3fd36d43 --- /dev/null +++ b/src/modules/feature/board-collaborative-text-editor-element/index.ts @@ -0,0 +1,3 @@ +import CollaborativeTextEditorElement from "./CollaborativeTextEditorElement.vue"; + +export { CollaborativeTextEditorElement }; diff --git a/src/modules/feature/board-collaborative-text-editor-element/test-utils/collaborativeTextEditorApiMock.ts b/src/modules/feature/board-collaborative-text-editor-element/test-utils/collaborativeTextEditorApiMock.ts new file mode 100644 index 0000000000..15979a9307 --- /dev/null +++ b/src/modules/feature/board-collaborative-text-editor-element/test-utils/collaborativeTextEditorApiMock.ts @@ -0,0 +1,23 @@ +import { jest } from "@jest/globals"; +import { useCollaborativeTextEditorApi } from "../composables/CollaborativeTextEditorApi.composable"; + +interface Props { + getUrlMock?: jest.Mock; +} + +export const setupCollaborativeTextEditorApiMock = (props: Props = {}) => { + const { getUrlMock } = props; + const mockedCollaborativeTextEditorApi = jest.mocked( + useCollaborativeTextEditorApi + ); + + const getUrl = getUrlMock ?? jest.fn(); + + const mocks = { + getUrl, + }; + + mockedCollaborativeTextEditorApi.mockReturnValue(mocks); + + return mocks; +}; diff --git a/src/modules/feature/board-collaborative-text-editor-element/test-utils/collaborativeTextEditorNotifier.ts b/src/modules/feature/board-collaborative-text-editor-element/test-utils/collaborativeTextEditorNotifier.ts new file mode 100644 index 0000000000..a19b01e21c --- /dev/null +++ b/src/modules/feature/board-collaborative-text-editor-element/test-utils/collaborativeTextEditorNotifier.ts @@ -0,0 +1,32 @@ +import { jest } from "@jest/globals"; +import { useCollaborativeTextEditorNotifier } from "../composables/CollaborativeTextEditorNotifications.composable"; + +interface Props { + showForbiddenErrorMock?: () => void; + showUnauthorizedErrorMock?: () => void; + showInternalServerErrorMock?: () => void; +} + +export const setupCollaborativeTextEditorNotifier = (props: Props = {}) => { + const { + showForbiddenErrorMock, + showUnauthorizedErrorMock, + showInternalServerErrorMock, + } = props; + + const mockedSelectedFile = jest.mocked(useCollaborativeTextEditorNotifier); + + const showForbiddenError = showForbiddenErrorMock ?? jest.fn(); + const showUnauthorizedError = showUnauthorizedErrorMock ?? jest.fn(); + const showInternalServerError = showInternalServerErrorMock ?? jest.fn(); + + const mocks = { + showForbiddenError, + showUnauthorizedError, + showInternalServerError, + }; + + mockedSelectedFile.mockReturnValue(mocks); + + return mocks; +}; diff --git a/src/modules/feature/board/card/ContentElementList.unit.ts b/src/modules/feature/board/card/ContentElementList.unit.ts index 56c29b528c..c29a163801 100644 --- a/src/modules/feature/board/card/ContentElementList.unit.ts +++ b/src/modules/feature/board/card/ContentElementList.unit.ts @@ -1,9 +1,14 @@ import { ContentElementType } from "@/serverApi/v3"; -import EnvConfigModule from "@/store/env-config"; import { ConfigResponse } from "@/serverApi/v3/api"; +import EnvConfigModule from "@/store/env-config"; import { AnyContentElement } from "@/types/board/ContentElement"; import { ENV_CONFIG_MODULE_KEY } from "@/utils/inject"; import { createModuleMocks } from "@/utils/mock-store-module"; +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; +import { CollaborativeTextEditorElement } from "@feature-board-collaborative-text-editor-element"; import { DrawingContentElement } from "@feature-board-drawing-element"; import { ExternalToolElement } from "@feature-board-external-tool-element"; import { FileContentElement } from "@feature-board-file-element"; @@ -13,10 +18,6 @@ import { RichTextContentElement } from "@feature-board-text-element"; import { createMock } from "@golevelup/ts-jest"; import { shallowMount } from "@vue/test-utils"; import ContentElementList from "./ContentElementList.vue"; -import { - createTestingI18n, - createTestingVuetify, -} from "@@/tests/test-utils/setup"; describe("ContentElementList", () => { const setup = (props: { @@ -83,6 +84,10 @@ describe("ContentElementList", () => { elementType: ContentElementType.Drawing, component: DrawingContentElement, }, + { + elementType: ContentElementType.CollaborativeTextEditor, + component: CollaborativeTextEditorElement, + }, ]; it.each(elementComponents)( diff --git a/src/modules/feature/board/card/ContentElementList.vue b/src/modules/feature/board/card/ContentElementList.vue index 950ad51ed8..289b8e17e1 100644 --- a/src/modules/feature/board/card/ContentElementList.vue +++ b/src/modules/feature/board/card/ContentElementList.vue @@ -61,6 +61,18 @@ @move-up:edit="onMoveElementUp(index, element)" @delete:element="onDeleteElement" /> + @@ -68,23 +80,25 @@