Files
kandev/apps/web/components/task/session-dialog-shared.test.tsx
T
José Almeida 85820e4a43 fix(web): warn when pasted attachments are too large (#1983)
* fix(web): warn when pasted attachment is too large

* test(web): provide toast context to chat input tests

* fix(web): warn when browser image paste lacks file data

* fix(web): warn about oversized new task attachments

* fix(web): keep toasts above modal overlays

* fix(web): handle file-list-only dialog pastes

* fix(web): avoid parsing clipboard HTML

* fix(web): handle attachment paste edge cases

* fix(web): improve attachment limit feedback

* fix(web): show aggregate attachment limit feedback
2026-07-27 17:19:02 +01:00

121 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { act, cleanup, renderHook, screen, waitFor } from "@testing-library/react";
import React from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ToastProvider } from "@/components/toast-provider";
import { MAX_FILES, MAX_FILE_SIZE, MAX_TOTAL_SIZE } from "./chat/file-attachment";
import { useDialogAttachments } from "./session-dialog-shared";
afterEach(cleanup);
const toastMessageTestId = "toast-message";
function fileWithSize(name: string, size: number) {
const file = new File(["attachment"], name, { type: "text/plain" });
Object.defineProperty(file, "size", { value: size });
return file;
}
describe("useDialogAttachments", () => {
it("warns when an image clipboard item has no readable file", async () => {
const preventDefault = vi.fn();
const clipboardData = {
files: [],
items: [{ kind: "file", type: "image/png", getAsFile: () => null }],
getData: () => "",
} as unknown as DataTransfer;
const { result } = renderHook(() => useDialogAttachments(false), {
wrapper: ({ children }) => React.createElement(ToastProvider, null, children),
});
await act(async () => {
result.current.handlePaste({
clipboardData,
preventDefault,
} as unknown as React.ClipboardEvent<HTMLTextAreaElement>);
});
expect(preventDefault).toHaveBeenCalledOnce();
expect(screen.getByTestId(toastMessageTestId).textContent).toContain(
"Pasted image couldn’t be attached",
);
expect(result.current.attachments).toEqual([]);
});
it("warns for an oversized clipboard file when item conversion provides no file", async () => {
const oversizedImage = new File(["image"], "copied.png", { type: "image/png" });
Object.defineProperty(oversizedImage, "size", { value: MAX_FILE_SIZE + 1 });
const preventDefault = vi.fn();
const clipboardData = {
files: [oversizedImage],
items: [{ kind: "file", type: "image/png", getAsFile: () => null }],
getData: () => "",
} as unknown as DataTransfer;
const { result } = renderHook(() => useDialogAttachments(false), {
wrapper: ({ children }) => React.createElement(ToastProvider, null, children),
});
await act(async () => {
result.current.handlePaste({
clipboardData,
preventDefault,
} as unknown as React.ClipboardEvent<HTMLTextAreaElement>);
});
expect(preventDefault).toHaveBeenCalledOnce();
expect(screen.getByTestId(toastMessageTestId).textContent).toContain("Attachment is too large");
expect(result.current.attachments).toEqual([]);
});
it("keeps fitting attachments and warns once when a batch exceeds the file count", async () => {
const { result } = renderHook(() => useDialogAttachments(false), {
wrapper: ({ children }) => React.createElement(ToastProvider, null, children),
});
const files = Array.from({ length: MAX_FILES + 1 }, (_, index) =>
fileWithSize(`attachment-${index}.txt`, 1),
);
await act(async () => {
await result.current.handleFileInputChange({
target: { files, value: "" },
} as unknown as React.ChangeEvent<HTMLInputElement>);
});
await waitFor(() => expect(result.current.attachments).toHaveLength(MAX_FILES));
expect(result.current.attachments.map((attachment) => attachment.fileName)).toEqual(
files.slice(0, MAX_FILES).map((file) => file.name),
);
expect(screen.getAllByTestId(toastMessageTestId)).toHaveLength(1);
expect(screen.getByTestId(toastMessageTestId).textContent).toContain(
`You can attach up to ${MAX_FILES} files.`,
);
});
it("keeps fitting attachments and warns once when a batch exceeds total size", async () => {
const { result } = renderHook(() => useDialogAttachments(false), {
wrapper: ({ children }) => React.createElement(ToastProvider, null, children),
});
const fileSize = MAX_TOTAL_SIZE / 3 + 1;
const files = [
fileWithSize("first.txt", fileSize),
fileWithSize("second.txt", fileSize),
fileWithSize("third.txt", fileSize),
];
await act(async () => {
await result.current.handleFileInputChange({
target: { files, value: "" },
} as unknown as React.ChangeEvent<HTMLInputElement>);
});
await waitFor(() => expect(result.current.attachments).toHaveLength(2));
expect(result.current.attachments.map((attachment) => attachment.fileName)).toEqual([
"first.txt",
"second.txt",
]);
expect(screen.getAllByTestId(toastMessageTestId)).toHaveLength(1);
expect(screen.getByTestId(toastMessageTestId).textContent).toContain(
`Attachments can total up to ${MAX_TOTAL_SIZE / 1024 / 1024} MB.`,
);
});
});