Files
siftlode/frontend/src/components/DownloadIntake.dom.test.tsx
T
peter 8b2260c9a8 fix(files): review round 1, the page — honest states, the keyboard, the tray's words
- A library past the list's cap is searched and sorted on the server; the count says "the newest".
- Each folder and each search opens on its first page (DataTable pageScope, useCardPager scope).
- A file dropped where the page takes none never reaches the browser (which would unload the app).
- The viewer's arrows wait while a layer opened from it is on top, and scroll a wide text line;
  a text shows its first 5000 lines and says where the rest is.
- Confirms name the file (no more "your downloads"); the asking menu rows end in "…".
- The tray names a running step beside its bar, shows a pause mark for paused work, polls slowly
  while only paused work waits, and forgets a finished file that was deleted.
- An unchanged rename sends nothing; a paste opens the download dialog only with nothing on top;
  the link box never replaces what was typed.
- Honest states: folders that failed to load, a file no longer there, the storage figure; the rail
  says "nothing downloaded" only when there is nothing.
- Focus after deleting the open folder and when the format form opens; the format name's label.
- ui/Meter (one danger line for both quota bars) and ui/RenameField in the platform layer.
- Copy fixes, stale comments and the speculative hub key; a contract test for the list's
  parameters; e2e: the Files page and the paste intake.
2026-10-13 00:14:09 +02:00

165 lines
5.7 KiB
TypeScript

// @vitest-environment jsdom
//
// Downloading is an action you start anywhere: a lone http(s) link pasted onto a page (not into a
// field, not at a dialog) opens the app's one download dialog; the Files link box hands its link to
// the same dialog; the palette's "Download from link…" focuses that box on the entry it navigated to.
// The dialog itself is stubbed (it names the link it was opened on).
import { afterEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
vi.mock("react-i18next", async (importOriginal) => ({
...(await importOriginal<typeof import("react-i18next")>()),
useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }),
}));
vi.mock("./DownloadDialog", () => ({
default: ({ source, onClose }: { source: string; onClose: () => void }) => (
<div role="dialog" aria-label="download">
{source}
<button onClick={onClose}>close</button>
</div>
),
}));
import DownloadIntake from "./DownloadIntake";
import { LinkBox } from "./LinkBox";
import { clearPendingDownload, requestLinkBox } from "../lib/downloadIntake";
import { ensureNavId } from "../lib/history";
import { pushLayer, removeLayer } from "../lib/layerStack";
afterEach(() => {
cleanup();
act(clearPendingDownload);
});
function paste(target: EventTarget, text: string) {
const event = new Event("paste", { bubbles: true, cancelable: true }) as ClipboardEvent;
Object.defineProperty(event, "clipboardData", {
value: { getData: (type: string) => (type === "text/plain" ? text : "") },
});
act(() => {
target.dispatchEvent(event);
});
return event;
}
const dialog = () => screen.queryByRole("dialog", { name: "download" });
describe("DownloadIntake · a pasted link", () => {
it("a link pasted onto the page opens the download dialog on it", async () => {
render(<DownloadIntake />);
const event = paste(document.body, " https://www.tiktok.com/@a/video/1 ");
expect(await screen.findByText("https://www.tiktok.com/@a/video/1")).toBeTruthy();
// Taken: the browser does nothing else with it.
expect(event.defaultPrevented).toBe(true);
});
it("text that is not one link, or a paste into a field, is left alone", async () => {
render(
<>
<DownloadIntake />
<input aria-label="search" />
</>,
);
paste(document.body, "just some words");
paste(document.body, "https://a.example/1 https://a.example/2");
const field = screen.getByRole("textbox", { name: "search" });
expect(paste(field, "https://a.example/1").defaultPrevented).toBe(false);
await act(async () => {});
expect(dialog()).toBeNull();
});
it("with a dialog on top, a paste is the dialog's, not a download", async () => {
render(
<>
<DownloadIntake />
<div aria-modal="true" />
</>,
);
paste(document.body, "https://a.example/1");
await act(async () => {});
expect(dialog()).toBeNull();
});
it("with a player or a menu on top (a layer, no aria-modal), a paste is not a download", async () => {
render(<DownloadIntake />);
const layer = pushLayer();
try {
paste(document.body, "https://a.example/1");
await act(async () => {});
expect(dialog()).toBeNull();
} finally {
removeLayer(layer);
}
});
it("closing the dialog closes it", async () => {
render(<DownloadIntake />);
paste(document.body, "https://a.example/1");
fireEvent.click(await screen.findByText("close"));
expect(dialog()).toBeNull();
});
});
describe("LinkBox", () => {
it("Enter hands the typed link to the dialog and empties the box", async () => {
render(
<>
<DownloadIntake />
<LinkBox />
</>,
);
const field = screen.getByRole("textbox", { name: "files.link.label" });
fireEvent.change(field, { target: { value: " https://vimeo.com/123 " } });
fireEvent.keyDown(field, { key: "Enter" });
expect(await screen.findByText("https://vimeo.com/123")).toBeTruthy();
expect((field as HTMLInputElement).value).toBe("");
});
it("a clipboard link read on focus never replaces what was typed while it was read", async () => {
let answer: (text: string) => void = () => {};
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { readText: () => new Promise<string>((resolve) => (answer = resolve)) },
});
render(<LinkBox />);
const box = screen.getByRole("textbox");
fireEvent.focus(box);
fireEvent.change(box, { target: { value: "https://typed.example/1" } });
await act(async () => answer("https://clipboard.example/2"));
expect((box as HTMLInputElement).value).toBe("https://typed.example/1");
});
it("an empty box downloads nothing", async () => {
render(
<>
<DownloadIntake />
<LinkBox />
</>,
);
fireEvent.click(screen.getByText("files.link.download"));
await act(async () => {});
expect(dialog()).toBeNull();
});
it("the palette's request focuses the box on its entry, once", () => {
ensureNavId();
act(requestLinkBox);
const { unmount } = render(<LinkBox />);
expect(document.activeElement).toBe(screen.getByRole("textbox", { name: "files.link.label" }));
unmount();
// Spent: the next mount on the same entry is not focused again.
render(<LinkBox />);
expect(document.activeElement).toBe(document.body);
});
it("a request made for another entry (the navigation never got here) focuses nothing", () => {
ensureNavId();
act(requestLinkBox);
// A later entry — say Back, then elsewhere — mounts the box.
window.history.pushState({}, "");
ensureNavId();
render(<LinkBox />);
expect(document.activeElement).toBe(document.body);
});
});