diff --git a/src/fileStorageApi/v3/api/file-api.ts b/src/fileStorageApi/v3/api/file-api.ts index fa22c9ca5a..5d7933eef8 100644 --- a/src/fileStorageApi/v3/api/file-api.ts +++ b/src/fileStorageApi/v3/api/file-api.ts @@ -432,6 +432,44 @@ export const FileApiAxiosParamCreator = function (configuration?: Configuration) + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Get file record meta data by file record id. + * @param {string} fileRecordId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getFileRecord: async (fileRecordId: string, options: any = {}): Promise => { + // verify required parameter 'fileRecordId' is not null or undefined + assertParamExists('getFileRecord', 'fileRecordId', fileRecordId) + const localVarPath = `/file/{fileRecordId}` + .replace(`{${"fileRecordId"}}`, encodeURIComponent(String(fileRecordId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -907,6 +945,17 @@ export const FileApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.downloadPreview(fileRecordId, fileName, outputFormat, width, forceUpdate, range, ifNoneMatch, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Get file record meta data by file record id. + * @param {string} fileRecordId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getFileRecord(fileRecordId: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getFileRecord(fileRecordId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Get stats (count and total size) of all files for a parent entityId. @@ -1109,6 +1158,16 @@ export const FileApiFactory = function (configuration?: Configuration, basePath? downloadPreview(fileRecordId: string, fileName: string, outputFormat?: PreviewOutputMimeTypes, width?: PreviewWidth, forceUpdate?: boolean, range?: string, ifNoneMatch?: string, options?: any): AxiosPromise { return localVarFp.downloadPreview(fileRecordId, fileName, outputFormat, width, forceUpdate, range, ifNoneMatch, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Get file record meta data by file record id. + * @param {string} fileRecordId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getFileRecord(fileRecordId: string, options?: any): AxiosPromise { + return localVarFp.getFileRecord(fileRecordId, options).then((request) => request(axios, basePath)); + }, /** * * @summary Get stats (count and total size) of all files for a parent entityId. @@ -1303,6 +1362,16 @@ export interface FileApiInterface { */ downloadPreview(fileRecordId: string, fileName: string, outputFormat?: PreviewOutputMimeTypes, width?: PreviewWidth, forceUpdate?: boolean, range?: string, ifNoneMatch?: string, options?: any): AxiosPromise; + /** + * + * @summary Get file record meta data by file record id. + * @param {string} fileRecordId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FileApiInterface + */ + getFileRecord(fileRecordId: string, options?: any): AxiosPromise; + /** * * @summary Get stats (count and total size) of all files for a parent entityId. @@ -1513,6 +1582,18 @@ export class FileApi extends BaseAPI implements FileApiInterface { return FileApiFp(this.configuration).downloadPreview(fileRecordId, fileName, outputFormat, width, forceUpdate, range, ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Get file record meta data by file record id. + * @param {string} fileRecordId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FileApi + */ + public getFileRecord(fileRecordId: string, options?: any) { + return FileApiFp(this.configuration).getFileRecord(fileRecordId, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Get stats (count and total size) of all files for a parent entityId. diff --git a/src/modules/data/file/FileRecords.state.ts b/src/modules/data/file/FileRecords.state.ts index 5b8ce63a1a..a760c193c2 100644 --- a/src/modules/data/file/FileRecords.state.ts +++ b/src/modules/data/file/FileRecords.state.ts @@ -16,6 +16,14 @@ export const useFileRecordsStore = defineStore("fileRecords", { return parentRecords ? Array.from(parentRecords.values()) : []; }, + getFileRecordById(fileRecordId: string): FileRecord | undefined { + for (const parentRecords of this.recordsByParent.values()) { + const record = parentRecords.get(fileRecordId); + + if (record) return record; + } + }, + upsertFileRecords(updatedRecords: FileRecord[]): void { updatedRecords.forEach((updatedRecord) => { const { parentId, id } = updatedRecord; 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.ts b/src/modules/data/file/FileStorageApi.composable.ts index 2a498bdd0f..5f731a24dc 100644 --- a/src/modules/data/file/FileStorageApi.composable.ts +++ b/src/modules/data/file/FileStorageApi.composable.ts @@ -31,10 +31,21 @@ export const useFileStorageApi = () => { const fileApi: FileApiInterface = FileApiFactory(undefined, "/v3", $axios); const wopiApi: WopiApiInterface = WopiApiFactory(undefined, "/v3", $axios); - const { getFileRecordsByParentId, upsertFileRecords, deleteFileRecords } = useFileRecordsStore(); + const { getFileRecordsByParentId, upsertFileRecords, deleteFileRecords, getFileRecordById } = useFileRecordsStore(); const { getStatisticByParentId, setStatisticForParent } = useParentStatisticsStore(); + const fetchFileById = async (fileRecordId: string): Promise => { + try { + const response = await fileApi.getFileRecord(fileRecordId); + + upsertFileRecords([response.data]); + } catch (error) { + showError(error); + throw error; + } + }; + const fetchFiles = async (parentId: string, parentType: FileRecordParent): Promise => { try { const schoolId = useAppStore().school?.id as string; @@ -173,9 +184,11 @@ export const useFileStorageApi = () => { upload, uploadFromUrl, getFileRecordsByParentId, + getFileRecordById, deleteFiles, getStatisticByParentId, tryGetParentStatisticFromApi: fetchFileStatistic, getAuthorizedCollaboraDocumentUrl, + fetchFileById, }; }; 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 = () => { 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); + }); + }); }); diff --git a/src/modules/page/collabora/Collabora.page.vue b/src/modules/page/collabora/Collabora.page.vue index 99e5c9b1cc..e16c0d9ea2 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')" /> @@ -12,20 +12,24 @@