85820e4a43
* 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
121 lines
4.6 KiB
TypeScript
121 lines
4.6 KiB
TypeScript
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.`,
|
||
);
|
||
});
|
||
});
|