UploadDialog (drag-drop + native picker + optional display name) opened from a new Upload button in the queue add-row. Uploads stream through a new XHR helper (uploadReq in api/core) so the dialog shows real 0-100% progress with a cancel — fetch/req can't report upload progress. The upload lands as an already-done job; the S5b file-kind row gates it to download+share. i18n HU/EN; upload cap/empty errors localize via the shared structured-detail path.
76 lines
3.0 KiB
TypeScript
76 lines
3.0 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// E7·S5c — the local-upload dialog. It's a general file upload (any type, no format list): a picked
|
|
// file surfaces its name+size, and Upload streams it through api.uploadDownload (the XHR-progress
|
|
// path). This drives the real dialog against a mocked api so we can assert the file + optional name
|
|
// actually reach the ingest call, and that the success toast points at the library. i18n is
|
|
// key-passthrough.
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
|
|
const h = vi.hoisted(() => ({
|
|
uploadDownload: vi.fn(async (_f: File, _o: unknown) => ({ id: 42 })),
|
|
notify: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("react-i18next", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
|
return { ...actual, useTranslation: () => ({ t: (key: string) => key }) };
|
|
});
|
|
vi.mock("../lib/api", () => ({ api: { uploadDownload: h.uploadDownload } }));
|
|
vi.mock("../lib/downloads", () => ({ invalidateDownloads: vi.fn() }));
|
|
vi.mock("../lib/notifications", () => ({ notify: h.notify }));
|
|
vi.mock("../lib/nav", () => ({ navigateTo: vi.fn() }));
|
|
|
|
import UploadDialog from "./UploadDialog";
|
|
|
|
const wrap = (onClose = vi.fn()) => {
|
|
const qc = new QueryClient();
|
|
return render(
|
|
<QueryClientProvider client={qc}>
|
|
<UploadDialog onClose={onClose} />
|
|
</QueryClientProvider>,
|
|
);
|
|
};
|
|
|
|
// The dialog portals into document.body, so query the whole document, not the render container.
|
|
const fileInput = () => document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
|
|
beforeEach(() => {
|
|
h.uploadDownload.mockClear();
|
|
h.notify.mockClear();
|
|
});
|
|
afterEach(cleanup);
|
|
|
|
describe("UploadDialog", () => {
|
|
it("shows the picked file's name and uploads it with the optional name", async () => {
|
|
const onClose = vi.fn();
|
|
wrap(onClose);
|
|
const f = new File(["hello world"], "notes.pdf", { type: "application/pdf" });
|
|
fireEvent.change(fileInput(), { target: { files: [f] } });
|
|
|
|
// The drop zone now surfaces the file name.
|
|
expect(screen.getByText("notes.pdf")).toBeTruthy();
|
|
|
|
// Optional display name.
|
|
const nameBox = screen.getByPlaceholderText("downloads.upload.namePlaceholder");
|
|
fireEvent.change(nameBox, { target: { value: "My notes" } });
|
|
|
|
fireEvent.click(screen.getByText("downloads.upload.submit"));
|
|
|
|
await waitFor(() => expect(h.uploadDownload).toHaveBeenCalledTimes(1));
|
|
const [passedFile, opts] = h.uploadDownload.mock.calls[0]!;
|
|
expect(passedFile).toBe(f);
|
|
expect((opts as { displayName?: string }).displayName).toBe("My notes");
|
|
await waitFor(() => expect(h.notify).toHaveBeenCalledTimes(1));
|
|
await waitFor(() => expect(onClose).toHaveBeenCalled());
|
|
});
|
|
|
|
it("keeps Upload disabled until a file is chosen", () => {
|
|
wrap();
|
|
const btn = screen.getByText("downloads.upload.submit").closest("button")!;
|
|
expect(btn.disabled).toBe(true);
|
|
});
|
|
});
|