From 20f08ac835a0a8286f0abf6cdc6b1239c4c6848a Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Fri, 17 Oct 2025 14:56:34 +0200 Subject: [PATCH 01/12] Add page title to collabora page --- .../board-file-element/FileContentElement.vue | 2 + src/modules/page/collabora/Collabora.page.vue | 50 ++++++++++++++++--- src/router/routes.ts | 2 + 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/modules/feature/board-file-element/FileContentElement.vue b/src/modules/feature/board-file-element/FileContentElement.vue index f44224af14..7b52170836 100644 --- a/src/modules/feature/board-file-element/FileContentElement.vue +++ b/src/modules/feature/board-file-element/FileContentElement.vue @@ -218,6 +218,8 @@ const openCollabora = () => { id: fileRecord.value.id, }, query: { + parentId: element.value.id, + fileName: fileRecord.value.name, editorMode, }, }).href; diff --git a/src/modules/page/collabora/Collabora.page.vue b/src/modules/page/collabora/Collabora.page.vue index 99e5c9b1cc..941fba12e5 100644 --- a/src/modules/page/collabora/Collabora.page.vue +++ b/src/modules/page/collabora/Collabora.page.vue @@ -3,7 +3,7 @@ ref="iframeRef" allow="clipboard-read *; clipboard-write *" allowfullscreen - :src="url" + :src="url.toString()" style="width: 100%; height: 100%; position: absolute" :title="$t('pages.collabora.iframeTitle')" /> @@ -11,21 +11,28 @@ diff --git a/src/router/routes.ts b/src/router/routes.ts index 0e4a16d218..50c71dbc8a 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -147,6 +147,8 @@ export const routes: Readonly[] = [ name: "collabora", props: (route: RouteLocationNormalized) => ({ fileRecordId: route.params.id, + parentId: route.query.parentId, + fileName: route.query.fileName, editorMode: route.query.editorMode, }), meta: { From af6715d60c8847530706e15e166398ae5ce5b28e Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Fri, 17 Oct 2025 15:05:30 +0200 Subject: [PATCH 02/12] Use board api --- src/modules/page/collabora/Collabora.page.vue | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/modules/page/collabora/Collabora.page.vue b/src/modules/page/collabora/Collabora.page.vue index 941fba12e5..4242800810 100644 --- a/src/modules/page/collabora/Collabora.page.vue +++ b/src/modules/page/collabora/Collabora.page.vue @@ -11,11 +11,10 @@ From 576cfd2630dd196e67a1f312f6a40a9bf2ccb3cc Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Mon, 20 Oct 2025 18:12:58 +0200 Subject: [PATCH 07/12] Fix param order --- src/modules/page/collabora/Collabora.page.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/page/collabora/Collabora.page.vue b/src/modules/page/collabora/Collabora.page.vue index 612659e4f2..b9f913747d 100644 --- a/src/modules/page/collabora/Collabora.page.vue +++ b/src/modules/page/collabora/Collabora.page.vue @@ -73,7 +73,7 @@ const setPageTitle = async () => { const fileRecord = getFileRecordById(props.fileRecordId); const parentName = await getParentName(fileRecord?.parentId); - const firstPartOfPageTitle = formatePageTitlePrefix(parentName, fileRecord?.name); + const firstPartOfPageTitle = formatePageTitlePrefix(fileRecord?.name, parentName); const pageTitle = buildPageTitle(firstPartOfPageTitle); useTitle(pageTitle); From 8c1ccc2c33dbf31076f468499699e3a8d5960206 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Tue, 21 Oct 2025 08:56:31 +0200 Subject: [PATCH 08/12] Remove url params --- src/modules/feature/board-file-element/FileContentElement.vue | 2 -- src/router/routes.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/modules/feature/board-file-element/FileContentElement.vue b/src/modules/feature/board-file-element/FileContentElement.vue index 7b52170836..f44224af14 100644 --- a/src/modules/feature/board-file-element/FileContentElement.vue +++ b/src/modules/feature/board-file-element/FileContentElement.vue @@ -218,8 +218,6 @@ const openCollabora = () => { id: fileRecord.value.id, }, query: { - parentId: element.value.id, - fileName: fileRecord.value.name, editorMode, }, }).href; diff --git a/src/router/routes.ts b/src/router/routes.ts index 50c71dbc8a..0e4a16d218 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -147,8 +147,6 @@ export const routes: Readonly[] = [ name: "collabora", props: (route: RouteLocationNormalized) => ({ fileRecordId: route.params.id, - parentId: route.query.parentId, - fileName: route.query.fileName, editorMode: route.query.editorMode, }), meta: { From da00cf7014f3dc562fef8bc23cdd5ee28079a20a Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Tue, 21 Oct 2025 12:42:39 +0200 Subject: [PATCH 09/12] Add store and api test --- .../data/file/FileRecords.state.unit.ts | 59 ++++++++++ .../file/FileStorageApi.composable.unit.ts | 110 ++++++++++++++++++ 2 files changed, 169 insertions(+) diff --git a/src/modules/data/file/FileRecords.state.unit.ts b/src/modules/data/file/FileRecords.state.unit.ts index 9a7c35c685..eb8e37a307 100644 --- a/src/modules/data/file/FileRecords.state.unit.ts +++ b/src/modules/data/file/FileRecords.state.unit.ts @@ -126,6 +126,65 @@ describe("FileRecords Store", () => { }); }); + describe("getFileRecordById", () => { + describe("when store is empty", () => { + it("should return undefined if no records exist", () => { + const store = useFileRecordsStore(); + + const result = store.getFileRecordById("nonexistent"); + + expect(result).toBeUndefined(); + }); + }); + + describe("when store has records", () => { + const setup = () => { + const store = useFileRecordsStore(); + + const record1 = fileRecordFactory.build(); + const record2 = fileRecordFactory.build(); + const record3 = fileRecordFactory.build(); + + return { + store, + record1, + record2, + record3, + }; + }; + + it("should return the correct file record when it exists", () => { + const { store, record1, record2, record3 } = setup(); + + store.upsertFileRecords([record1, record2, record3]); + + const result = store.getFileRecordById(record2.id); + + expect(result).toEqual(expect.objectContaining(record2)); + }); + + it("should return the correct file record from different parent ids", () => { + const { store, record1, record2, record3 } = setup(); + + store.upsertFileRecords([record1, record2, record3]); + + const result = store.getFileRecordById(record3.id); + + expect(result).toEqual(expect.objectContaining(record3)); + }); + + it("should return undefined if the file record does not exist", () => { + const { store, record1, record2, record3 } = setup(); + + store.upsertFileRecords([record1, record2, record3]); + + const result = store.getFileRecordById("nonexistent"); + + expect(result).toBeUndefined(); + }); + }); + }); + describe("deleteFileRecords", () => { describe("when store is empty", () => { it("should not throw an error when deleting from an empty store", () => { diff --git a/src/modules/data/file/FileStorageApi.composable.unit.ts b/src/modules/data/file/FileStorageApi.composable.unit.ts index 5330dc24f9..84c9618a5b 100644 --- a/src/modules/data/file/FileStorageApi.composable.unit.ts +++ b/src/modules/data/file/FileStorageApi.composable.unit.ts @@ -88,6 +88,116 @@ describe("FileStorageApi Composable", () => { }); }); + describe("fetchFileById", () => { + describe("when file api returns file record successfully", () => { + const setup = () => { + const fileRecord = fileRecordFactory.build(); + const response = createMock>({ + data: fileRecord, + }); + + const fileApi = createMock(); + vi.spyOn(serverApi, "FileApiFactory").mockReturnValueOnce(fileApi); + fileApi.getFileRecord.mockResolvedValueOnce(response); + + return { + fileRecord, + response, + fileApi, + }; + }; + + it("should call FileApiFactory.getFileRecord", async () => { + const { fileApi, fileRecord } = setup(); + const { fetchFileById } = useFileStorageApi(); + + await fetchFileById(fileRecord.id); + + expect(fileApi.getFileRecord).toHaveBeenCalledWith(fileRecord.id); + }); + + it("should upsert filerecord", async () => { + const { fileRecord } = setup(); + const { fetchFileById, getFileRecordById } = useFileStorageApi(); + + await fetchFileById(fileRecord.id); + + const result = getFileRecordById(fileRecord.id); + expect(result).toStrictEqual(fileRecord); + }); + }); + + describe("when file api returns error", () => { + const setup = (message?: string) => { + const fileRecordId = ObjectIdMock(); + + const { responseError, expectedPayload } = setupErrorResponse(message); + mockedMapAxiosErrorToResponseError.mockReturnValueOnce(expectedPayload); + + const fileApi = createMock(); + vi.spyOn(serverApi, "FileApiFactory").mockReturnValueOnce(fileApi); + fileApi.getFileRecord.mockRejectedValue(responseError); + + return { + fileRecordId, + responseError, + }; + }; + + it("should notify unauthorized and pass error", async () => { + const { fileRecordId, responseError } = setup(ErrorType.Unauthorized); + + const { fetchFileById } = useFileStorageApi(); + + await expect(fetchFileById(fileRecordId)).rejects.toBe(responseError); + expect(useNotificationStore().notify).toHaveBeenCalledWith( + expect.objectContaining({ status: "error", text: "error.401" }) + ); + }); + + it("should notify forbidden error and pass error", async () => { + const { fileRecordId, responseError } = setup(ErrorType.Forbidden); + + const { fetchFileById } = useFileStorageApi(); + + await expect(fetchFileById(fileRecordId)).rejects.toBe(responseError); + + expect(useNotificationStore().notify).toHaveBeenCalledWith( + expect.objectContaining({ status: "error", text: "error.403" }) + ); + }); + + it("should notify file not found error and pass error", async () => { + const { fileRecordId, responseError } = setup(ErrorType.FILE_NOT_FOUND); + + const { fetchFileById } = useFileStorageApi(); + + await expect(fetchFileById(fileRecordId)).rejects.toBe(responseError); + + expect(useNotificationStore().notify).toHaveBeenCalledWith( + expect.objectContaining({ + status: "error", + text: "components.board.notifications.errors.fileServiceNotAvailable", + }) + ); + }); + + it("should notify internal server error and pass error", async () => { + const { fileRecordId, responseError } = setup(); + const { fetchFileById } = useFileStorageApi(); + + await expect(fetchFileById(fileRecordId)).rejects.toBe(responseError); + + expect(useNotificationStore().notify).toHaveBeenCalledWith( + expect.objectContaining({ + status: "error", + text: "components.board.notifications.errors.fileServiceNotAvailable", + }) + ); + }); + }); + }); + describe("fetchFiles", () => { describe("when file api returns list successfully", () => { const setup = () => { From 0806af561f5e748e873360ff0e18a132a04917df Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Tue, 21 Oct 2025 15:45:51 +0200 Subject: [PATCH 10/12] Refactor getFileRecordById to use direct lookup for improved performance --- src/modules/data/file/FileRecords.state.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/data/file/FileRecords.state.ts b/src/modules/data/file/FileRecords.state.ts index 353fb34743..a760c193c2 100644 --- a/src/modules/data/file/FileRecords.state.ts +++ b/src/modules/data/file/FileRecords.state.ts @@ -16,11 +16,11 @@ export const useFileRecordsStore = defineStore("fileRecords", { return parentRecords ? Array.from(parentRecords.values()) : []; }, - getFileRecordById(id: string): FileRecord | undefined { + getFileRecordById(fileRecordId: string): FileRecord | undefined { for (const parentRecords of this.recordsByParent.values()) { - for (const [recordId, record] of parentRecords) { - if (recordId === id) return record; - } + const record = parentRecords.get(fileRecordId); + + if (record) return record; } }, From ac4b11d1482f9c16f117b4af432a2b1e1f7e30a6 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Wed, 22 Oct 2025 11:06:57 +0200 Subject: [PATCH 11/12] Add error handling to set page title handling --- src/modules/page/collabora/Collabora.page.vue | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/modules/page/collabora/Collabora.page.vue b/src/modules/page/collabora/Collabora.page.vue index b9f913747d..e16c0d9ea2 100644 --- a/src/modules/page/collabora/Collabora.page.vue +++ b/src/modules/page/collabora/Collabora.page.vue @@ -68,17 +68,32 @@ const setCollaboraUrl = async () => { }; const setPageTitle = async () => { - await fetchFileById(props.fileRecordId); - - const fileRecord = getFileRecordById(props.fileRecordId); - const parentName = await getParentName(fileRecord?.parentId); + let fileRecord; + let parentName; + + try { + fileRecord = await getFileRecord(props.fileRecordId); + parentName = await getParentName(fileRecord?.parentId); + } catch { + // Ignore errors here, because not critical + } const firstPartOfPageTitle = formatePageTitlePrefix(fileRecord?.name, parentName); const pageTitle = buildPageTitle(firstPartOfPageTitle); - useTitle(pageTitle); }; +const getFileRecord = async (fileId: string) => { + let fileRecord = getFileRecordById(fileId); + + if (!fileRecord) { + await fetchFileById(fileId); + fileRecord = getFileRecordById(fileId); + } + + return fileRecord; +}; + const formatePageTitlePrefix = (fileName?: string, parentName?: string) => { if (fileName) { return parentName ? `${fileName} - ${parentName}` : fileName; From aebfdc59f9e6e3b5590a3d28990a13a65c25f835 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Wed, 22 Oct 2025 11:07:42 +0200 Subject: [PATCH 12/12] Add collabora page test --- .../page/collabora/Collabora.page.unit.ts | 427 +++++++++++++++++- 1 file changed, 426 insertions(+), 1 deletion(-) diff --git a/src/modules/page/collabora/Collabora.page.unit.ts b/src/modules/page/collabora/Collabora.page.unit.ts index 3386bc8315..80810a8607 100644 --- a/src/modules/page/collabora/Collabora.page.unit.ts +++ b/src/modules/page/collabora/Collabora.page.unit.ts @@ -1,24 +1,53 @@ import CollaboraPage from "./Collabora.page.vue"; +import * as serverApi from "@/serverApi/v3/api"; import { EditorMode } from "@/types/file/File"; +import { buildPageTitle } from "@/utils/pageTitle"; import { authorizedCollaboraDocumentUrlResponseFactory, createTestAppStoreWithUser, expectNotification, + fileElementResponseFactory, + fileRecordFactory, ObjectIdMock, + parentNodeInfoFactory, } from "@@/tests/test-utils"; import { createTestingI18n, createTestingVuetify } from "@@/tests/test-utils/setup"; import { useAppStore } from "@data-app"; import * as FileStorageApi from "@data-file"; import { createMock } from "@golevelup/ts-vitest"; import { createTestingPinia } from "@pinia/testing"; -import { flushPromises } from "@vue/test-utils"; +import { flushPromises, mount } from "@vue/test-utils"; +import { useTitle } from "@vueuse/core"; +import { AxiosPromise } from "axios"; import { setActivePinia } from "pinia"; import { beforeEach } from "vitest"; +// Mock useTitle from @vueuse/core +vi.mock("@vueuse/core", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useTitle: vi.fn(), + }; +}); + +vi.mock("@/utils/pageTitle", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + buildPageTitle: vi.fn(), + }; +}); + +const mockUseTitle = vi.mocked(useTitle); +const mockBuildPageTitle = vi.mocked(buildPageTitle); + describe("Collabora.page", () => { beforeEach(() => { setActivePinia(createTestingPinia()); vi.resetAllMocks(); + mockUseTitle.mockClear(); + mockBuildPageTitle.mockClear(); }); const setup = () => { @@ -34,6 +63,17 @@ describe("Collabora.page", () => { authorizedCollaboraDocumentUrlResponse.authorizedCollaboraDocumentUrl ); + const fileElement = fileElementResponseFactory.build(); + const parentNodeInfos = parentNodeInfoFactory.build(); + const boardApi = createMock(); + boardApi.elementControllerGetElementWithParentHierarchy.mockReturnValueOnce({ + data: { + element: fileElement, + parentHierarchy: parentNodeInfos, + }, + } as unknown as AxiosPromise); + vi.spyOn(serverApi, "BoardElementApiFactory").mockReturnValueOnce(boardApi); + const wrapper = mount(CollaboraPage, { global: { plugins: [createTestingVuetify(), createTestingI18n()], @@ -75,6 +115,70 @@ describe("Collabora.page", () => { ); }); + describe("when editor mode prop is undefined", () => { + const setup = () => { + const fileRecordId = ObjectIdMock(); + const editorMode = EditorMode.EDIT; + const authorizedCollaboraDocumentUrlResponse = authorizedCollaboraDocumentUrlResponseFactory.build(); + + const { mockedMe } = createTestAppStoreWithUser("user-id"); + + const fileStorageApiMock = createMock>(); + vi.spyOn(FileStorageApi, "useFileStorageApi").mockReturnValueOnce(fileStorageApiMock); + fileStorageApiMock.getAuthorizedCollaboraDocumentUrl.mockResolvedValueOnce( + authorizedCollaboraDocumentUrlResponse.authorizedCollaboraDocumentUrl + ); + + const fileElement = fileElementResponseFactory.build(); + const parentNodeInfos = parentNodeInfoFactory.build(); + const boardApi = createMock(); + boardApi.elementControllerGetElementWithParentHierarchy.mockReturnValueOnce({ + data: { + element: fileElement, + parentHierarchy: parentNodeInfos, + }, + } as unknown as AxiosPromise); + vi.spyOn(serverApi, "BoardElementApiFactory").mockReturnValueOnce(boardApi); + + const wrapper = mount(CollaboraPage, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + propsData: { + fileRecordId, + editorMode: undefined, + }, + }); + + return { + wrapper, + authorizedCollaboraDocumentUrlResponse, + editorMode, + fileStorageApiMock, + fileRecordId, + mockedMe, + }; + }; + + it("should render Collabora editor iframe with view mode", async () => { + const { wrapper, authorizedCollaboraDocumentUrlResponse, fileStorageApiMock, fileRecordId, mockedMe } = + await setup(); + + await flushPromises(); + + expect(fileStorageApiMock.getAuthorizedCollaboraDocumentUrl).toHaveBeenCalledWith( + fileRecordId, + EditorMode.VIEW, + `${mockedMe.user.firstName} ${mockedMe.user.lastName}` + ); + + expect(wrapper.find("iframe").exists()).toBe(true); + expect(wrapper.find("iframe").attributes("src")).toEqual( + authorizedCollaboraDocumentUrlResponse.authorizedCollaboraDocumentUrl + `?lang=${useAppStore().locale}` + ); + }); + }); + describe("when iframe emits message", () => { describe("when message is not a collabora message", () => { describe("when MessageId is missing", () => { @@ -146,4 +250,325 @@ describe("Collabora.page", () => { }); }); }); + + describe("when file record exists in store", () => { + const setup = () => { + const editorMode = EditorMode.EDIT; + const authorizedCollaboraDocumentUrlResponse = authorizedCollaboraDocumentUrlResponseFactory.build(); + + createTestAppStoreWithUser("user-id"); + + const fileStorageApiMock = createMock>(); + vi.spyOn(FileStorageApi, "useFileStorageApi").mockReturnValueOnce(fileStorageApiMock); + fileStorageApiMock.getAuthorizedCollaboraDocumentUrl.mockResolvedValueOnce( + authorizedCollaboraDocumentUrlResponse.authorizedCollaboraDocumentUrl + ); + + const fileRecord = fileRecordFactory.build(); + fileStorageApiMock.getFileRecordById.mockReturnValue(fileRecord); + + const fileElement = fileElementResponseFactory.build(); + const parentNodeInfos = parentNodeInfoFactory.build(); + const boardApi = createMock(); + boardApi.elementControllerGetElementWithParentHierarchy.mockReturnValueOnce({ + data: { + element: fileElement, + parentHierarchy: [parentNodeInfos], + }, + } as unknown as AxiosPromise); + vi.spyOn(serverApi, "BoardElementApiFactory").mockReturnValueOnce(boardApi); + + mockBuildPageTitle.mockReturnValue("fetched-file.xlsx - Course Board - Instance Title"); + + mount(CollaboraPage, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + propsData: { + fileRecordId: fileRecord.id, + editorMode, + }, + }); + + return { + fileRecord, + parentNodeInfos, + }; + }; + + it("should call useTitle with page title including file name and parent name", async () => { + const { fileRecord, parentNodeInfos } = setup(); + + await flushPromises(); + + expect(mockBuildPageTitle).toHaveBeenCalledWith(`${fileRecord.name} - ${parentNodeInfos.name}`); + expect(mockUseTitle).toHaveBeenCalledWith("fetched-file.xlsx - Course Board - Instance Title"); + }); + }); + + describe("when file record needs to be fetched", () => { + const setup = () => { + const editorMode = EditorMode.EDIT; + const authorizedCollaboraDocumentUrlResponse = authorizedCollaboraDocumentUrlResponseFactory.build(); + + createTestAppStoreWithUser("user-id"); + + const fileStorageApiMock = createMock>(); + vi.spyOn(FileStorageApi, "useFileStorageApi").mockReturnValueOnce(fileStorageApiMock); + fileStorageApiMock.getAuthorizedCollaboraDocumentUrl.mockResolvedValueOnce( + authorizedCollaboraDocumentUrlResponse.authorizedCollaboraDocumentUrl + ); + + const fileRecord = fileRecordFactory.build(); + // First call returns null, then returns the file record after fetch + fileStorageApiMock.getFileRecordById.mockReturnValueOnce(undefined).mockReturnValueOnce(fileRecord); + fileStorageApiMock.fetchFileById.mockResolvedValueOnce(undefined); + + const fileElement = fileElementResponseFactory.build(); + const parentNodeInfos = parentNodeInfoFactory.build({ name: "Course Board" }); + const boardApi = createMock(); + boardApi.elementControllerGetElementWithParentHierarchy.mockReturnValueOnce({ + data: { + element: fileElement, + parentHierarchy: [parentNodeInfos], + }, + } as unknown as AxiosPromise); + vi.spyOn(serverApi, "BoardElementApiFactory").mockReturnValueOnce(boardApi); + + mockBuildPageTitle.mockReturnValueOnce("fetched-file.xlsx - Course Board - Instance Title"); + + mount(CollaboraPage, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + propsData: { + fileRecordId: fileRecord.id, + editorMode, + }, + }); + + return { + fileRecord, + fileStorageApiMock, + parentNodeInfos, + }; + }; + + it("should fetch file and call useTitle with correct parameters", async () => { + const { fileStorageApiMock, fileRecord, parentNodeInfos } = setup(); + + await flushPromises(); + + expect(fileStorageApiMock.fetchFileById).toHaveBeenCalled(); + expect(mockBuildPageTitle).toHaveBeenCalledWith(`${fileRecord.name} - ${parentNodeInfos.name}`); + expect(mockUseTitle).toHaveBeenCalledWith("fetched-file.xlsx - Course Board - Instance Title"); + }); + }); + + describe("when fetchFileById rejects", () => { + const setup = () => { + const editorMode = EditorMode.EDIT; + const authorizedCollaboraDocumentUrlResponse = authorizedCollaboraDocumentUrlResponseFactory.build(); + + createTestAppStoreWithUser("user-id"); + + const fileStorageApiMock = createMock>(); + vi.spyOn(FileStorageApi, "useFileStorageApi").mockReturnValueOnce(fileStorageApiMock); + fileStorageApiMock.getAuthorizedCollaboraDocumentUrl.mockResolvedValueOnce( + authorizedCollaboraDocumentUrlResponse.authorizedCollaboraDocumentUrl + ); + + const fileRecord = fileRecordFactory.build(); + // First call returns null, then returns the file record after fetch + fileStorageApiMock.getFileRecordById.mockReturnValueOnce(undefined); + fileStorageApiMock.fetchFileById.mockRejectedValueOnce(); + + const boardApi = createMock(); + vi.spyOn(serverApi, "BoardElementApiFactory").mockReturnValueOnce(boardApi); + + const expectedTitle = "standalone-file.pdf - Instance Title"; + mockBuildPageTitle.mockReturnValueOnce(expectedTitle); + + mount(CollaboraPage, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + propsData: { + fileRecordId: fileRecord.id, + editorMode, + }, + }); + + return { + fileRecord, + expectedTitle, + }; + }; + + it("should call useTitle with only file name", async () => { + const { expectedTitle } = setup(); + + await flushPromises(); + + expect(mockBuildPageTitle).toHaveBeenCalledWith(""); + expect(mockUseTitle).toHaveBeenCalledWith(expectedTitle); + }); + }); + + describe("when getElementWithParentHierarchy rejects", () => { + const setup = () => { + const editorMode = EditorMode.EDIT; + const authorizedCollaboraDocumentUrlResponse = authorizedCollaboraDocumentUrlResponseFactory.build(); + + createTestAppStoreWithUser("user-id"); + + const fileStorageApiMock = createMock>(); + vi.spyOn(FileStorageApi, "useFileStorageApi").mockReturnValueOnce(fileStorageApiMock); + fileStorageApiMock.getAuthorizedCollaboraDocumentUrl.mockResolvedValueOnce( + authorizedCollaboraDocumentUrlResponse.authorizedCollaboraDocumentUrl + ); + + const fileRecord = fileRecordFactory.build(); + fileStorageApiMock.getFileRecordById.mockReturnValueOnce(fileRecord); + + const boardApi = createMock(); + vi.spyOn(serverApi, "BoardElementApiFactory").mockReturnValueOnce(boardApi); + boardApi.elementControllerGetElementWithParentHierarchy.mockRejectedValueOnce(); + + const expectedTitle = "standalone-file.pdf - Instance Title"; + mockBuildPageTitle.mockReturnValueOnce(expectedTitle); + + mount(CollaboraPage, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + propsData: { + fileRecordId: fileRecord.id, + editorMode, + }, + }); + + return { + fileRecord, + expectedTitle, + }; + }; + + it("should call useTitle with only file name", async () => { + const { fileRecord, expectedTitle } = setup(); + + await flushPromises(); + + expect(mockBuildPageTitle).toHaveBeenCalledWith(`${fileRecord.name}`); + expect(mockUseTitle).toHaveBeenCalledWith(expectedTitle); + }); + }); + + describe("when file record has no name", () => { + const setup = () => { + const editorMode = EditorMode.EDIT; + const authorizedCollaboraDocumentUrlResponse = authorizedCollaboraDocumentUrlResponseFactory.build(); + + createTestAppStoreWithUser("user-id"); + + const fileStorageApiMock = createMock>(); + vi.spyOn(FileStorageApi, "useFileStorageApi").mockReturnValueOnce(fileStorageApiMock); + fileStorageApiMock.getAuthorizedCollaboraDocumentUrl.mockResolvedValueOnce( + authorizedCollaboraDocumentUrlResponse.authorizedCollaboraDocumentUrl + ); + + const fileRecord = fileRecordFactory.build({ + name: undefined, + }); + fileStorageApiMock.getFileRecordById.mockReturnValueOnce(undefined).mockReturnValueOnce(fileRecord); + + const fileElement = fileElementResponseFactory.build(); + const parentNodeInfos = parentNodeInfoFactory.build(); + const boardApi = createMock(); + boardApi.elementControllerGetElementWithParentHierarchy.mockReturnValueOnce({ + data: { + element: fileElement, + parentHierarchy: [parentNodeInfos], + }, + } as unknown as AxiosPromise); + vi.spyOn(serverApi, "BoardElementApiFactory").mockReturnValueOnce(boardApi); + + const expectedTitle = "Task Board - Instance Title"; + mockBuildPageTitle.mockReturnValue(expectedTitle); + + const wrapper = mount(CollaboraPage, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + propsData: { + fileRecordId: ObjectIdMock(), + editorMode, + }, + }); + + return { + wrapper, + parentNodeInfos, + expectedTitle, + }; + }; + + it("should call useTitle with only parent name when file name is missing", async () => { + const { parentNodeInfos, expectedTitle } = setup(); + + await flushPromises(); + + expect(mockBuildPageTitle).toHaveBeenCalledWith(`${parentNodeInfos.name}`); + expect(mockUseTitle).toHaveBeenCalledWith(expectedTitle); + }); + }); + + describe("when both file name and parent name are missing", () => { + const setup = () => { + const fileRecordId = ObjectIdMock(); + const editorMode = EditorMode.EDIT; + const authorizedCollaboraDocumentUrlResponse = authorizedCollaboraDocumentUrlResponseFactory.build(); + + const { mockedMe } = createTestAppStoreWithUser("user-id"); + + const fileStorageApiMock = createMock>(); + vi.spyOn(FileStorageApi, "useFileStorageApi").mockReturnValueOnce(fileStorageApiMock); + fileStorageApiMock.getAuthorizedCollaboraDocumentUrl.mockResolvedValueOnce( + authorizedCollaboraDocumentUrlResponse.authorizedCollaboraDocumentUrl + ); + + fileStorageApiMock.getFileRecordById.mockReturnValueOnce(undefined).mockReturnValueOnce(undefined); + + const boardApi = createMock(); + vi.spyOn(serverApi, "BoardElementApiFactory").mockReturnValueOnce(boardApi); + + const instanceTitle = "Instance Title"; + mockBuildPageTitle.mockReturnValue(instanceTitle); + + const wrapper = mount(CollaboraPage, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + propsData: { + fileRecordId, + editorMode, + }, + }); + + return { + wrapper, + mockedMe, + instanceTitle, + }; + }; + + it("should call useTitle with empty string when both file name and parent name are missing", async () => { + const { instanceTitle } = setup(); + + await flushPromises(); + + expect(mockBuildPageTitle).toHaveBeenCalledWith(""); + expect(mockUseTitle).toHaveBeenCalledWith(instanceTitle); + }); + }); });