- 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.
165 lines
5.7 KiB
TypeScript
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);
|
|
});
|
|
});
|