diff --git a/frontend/src/core/components/fileEditor/FileEditorFileName.test.tsx b/frontend/src/core/components/fileEditor/FileEditorFileName.test.tsx new file mode 100644 index 000000000..4d153ef08 --- /dev/null +++ b/frontend/src/core/components/fileEditor/FileEditorFileName.test.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { describe, expect, test } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { MantineProvider } from "@mantine/core"; +import FileEditorFileName from "@app/components/fileEditor/FileEditorFileName"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +const buildFileStub = ( + overrides: Partial = {}, +): StirlingFileStub => ({ + id: "file-1" as FileId, + name: "report.pdf", + type: "application/pdf", + size: 1024, + lastModified: 0, + isLeaf: true, + originalFileId: "file-1", + versionNumber: 1, + ...overrides, +}); + +const renderName = (file: StirlingFileStub) => + render( + + + , + ); + +describe("FileEditorFileName (core / web)", () => { + test.each([ + { name: "not-saved", file: buildFileStub() }, + { + name: "dirty", + file: buildFileStub({ localFilePath: "/tmp/report.pdf", isDirty: true }), + }, + { + name: "saved", + file: buildFileStub({ localFilePath: "/tmp/report.pdf", isDirty: false }), + }, + ])("does not render a save indicator ($name)", ({ file }) => { + renderName(file); + + expect(screen.queryByLabelText("fileNotSavedToDisk")).toBeNull(); + expect(screen.queryByLabelText("unsavedChanges")).toBeNull(); + expect(screen.queryByLabelText("fileSavedToDisk")).toBeNull(); + }); + + test("renders the filename", () => { + renderName(buildFileStub({ name: "invoice.pdf" })); + expect(screen.getByText("invoice.pdf")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/core/components/fileEditor/FileEditorFileName.tsx b/frontend/src/core/components/fileEditor/FileEditorFileName.tsx index b526e8561..89c2e7f08 100644 --- a/frontend/src/core/components/fileEditor/FileEditorFileName.tsx +++ b/frontend/src/core/components/fileEditor/FileEditorFileName.tsx @@ -1,13 +1,18 @@ import React from "react"; import { StirlingFileStub } from "@app/types/fileContext"; import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { truncateCenter } from "@app/utils/textUtils"; interface FileEditorFileNameProps { file: StirlingFileStub; + maxLength?: number; } -const FileEditorFileName = ({ file }: FileEditorFileNameProps) => ( - {file.name} +const FileEditorFileName = ({ + file, + maxLength = 40, +}: FileEditorFileNameProps) => ( + {truncateCenter(file.name, maxLength)} ); export default FileEditorFileName; diff --git a/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx b/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx index 81ea1d27d..cf5ca7da2 100644 --- a/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx @@ -45,7 +45,7 @@ import { PrivateContent } from "@app/components/shared/PrivateContent"; import UploadToServerModal from "@app/components/shared/UploadToServerModal"; import ShareFileModal from "@app/components/shared/ShareFileModal"; import { useAppConfig } from "@app/contexts/AppConfigContext"; -import { truncateCenter } from "@app/utils/textUtils"; +import FileEditorFileName from "@app/components/fileEditor/FileEditorFileName"; interface FileEditorThumbnailProps { file: StirlingFileStub; @@ -580,8 +580,19 @@ const FileEditorThumbnail = ({ marginBottom: "0.5rem", }} > - - {truncateCenter(file.name, 40)} + + = {}, +): StirlingFileStub => ({ + id: "file-1" as FileId, + name: "report.pdf", + type: "application/pdf", + size: 1024, + lastModified: 0, + isLeaf: true, + originalFileId: "file-1", + versionNumber: 1, + ...overrides, +}); + +const renderName = (file: StirlingFileStub) => + render( + + + , + ); + +describe("FileEditorFileName (desktop)", () => { + test("renders red 'not saved' indicator when file has no local path", () => { + renderName(buildFileStub()); + + const indicator = screen.getByLabelText("fileNotSavedToDisk"); + expect(indicator).toBeInTheDocument(); + expect(indicator).toHaveStyle({ + backgroundColor: "var(--mantine-color-red-6)", + }); + expect(screen.queryByLabelText("unsavedChanges")).toBeNull(); + expect(screen.queryByLabelText("fileSavedToDisk")).toBeNull(); + }); + + test("renders yellow 'unsaved changes' indicator when file is dirty", () => { + renderName( + buildFileStub({ localFilePath: "/tmp/report.pdf", isDirty: true }), + ); + + const indicator = screen.getByLabelText("unsavedChanges"); + expect(indicator).toBeInTheDocument(); + expect(indicator).toHaveStyle({ + backgroundColor: "var(--mantine-color-yellow-6)", + }); + expect(screen.queryByLabelText("fileNotSavedToDisk")).toBeNull(); + expect(screen.queryByLabelText("fileSavedToDisk")).toBeNull(); + }); + + test("renders green 'saved' indicator when file is persisted and clean", () => { + renderName( + buildFileStub({ localFilePath: "/tmp/report.pdf", isDirty: false }), + ); + + const indicator = screen.getByLabelText("fileSavedToDisk"); + expect(indicator).toBeInTheDocument(); + expect(indicator).toHaveStyle({ + backgroundColor: "var(--mantine-color-green-6)", + }); + expect(screen.queryByLabelText("fileNotSavedToDisk")).toBeNull(); + expect(screen.queryByLabelText("unsavedChanges")).toBeNull(); + }); + + test("renders the filename alongside the indicator", () => { + renderName(buildFileStub({ name: "invoice.pdf" })); + expect(screen.getByText("invoice.pdf")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/desktop/components/fileEditor/FileEditorFileName.tsx b/frontend/src/desktop/components/fileEditor/FileEditorFileName.tsx index c173a4dc2..46349fd31 100644 --- a/frontend/src/desktop/components/fileEditor/FileEditorFileName.tsx +++ b/frontend/src/desktop/components/fileEditor/FileEditorFileName.tsx @@ -3,17 +3,22 @@ import { Tooltip } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { StirlingFileStub } from "@app/types/fileContext"; import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { truncateCenter } from "@app/utils/textUtils"; interface FileEditorFileNameProps { file: StirlingFileStub; + maxLength?: number; } -const FileEditorFileName = ({ file }: FileEditorFileNameProps) => { +const FileEditorFileName = ({ + file, + maxLength = 40, +}: FileEditorFileNameProps) => { const { t } = useTranslation(); return ( <> - {file.name} + {truncateCenter(file.name, maxLength)} {!file.localFilePath && (