Files could only hand you the bytes: double-clicking a picture put it on disk and left you to find it. The viewer for this already existed -- images, PDFs and text, in the attachment preview in MessageView -- it was just wired to one screen. It is now a component both screens use. Two things it needed before it was any use on Files. The type detection falls back to the file name: an upload carries whatever the browser guessed, which for anything unusual is application/octet-stream, so the old exact-type check saw nothing to show in a .md that had just been uploaded. And text is read with fetch, which ignores Content-Disposition, so Markdown previews even though the server will not serve it inline. Whether we can show a file and whether the server will serve it inline are separate questions, and lib/preview.ts answers them separately: `openableInTab` mirrors the isInlineSafe allowlist in the blob route, because navigating to a blob the server will not inline just starts a download. SVG is left out of both -- it carries script, and how to show one safely is its own question, not a detail of a file lister. Printing goes with it. A picture or a text file prints from the dialog with everything else dropped; a PDF prints itself from its own iframe, since the page around it cannot paginate someone else's document. Hiding `.app` alone was not enough there -- `#root` kept its height and printed a blank first page, the same trap as the message card.
81 lines
3.7 KiB
TypeScript
81 lines
3.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { openableInTab, previewKind } from "@/lib/preview";
|
|
|
|
describe("previewKind", () => {
|
|
it("goes by the declared type when there is one", () => {
|
|
expect(previewKind("image/png", "photo.png")).toBe("image");
|
|
expect(previewKind("application/pdf", "invoice.pdf")).toBe("pdf");
|
|
expect(previewKind("text/plain", "notes.txt")).toBe("text");
|
|
expect(previewKind("text/markdown", "README.md")).toBe("text");
|
|
expect(previewKind("application/json", "data.json")).toBe("text");
|
|
expect(previewKind("image/png; charset=binary", "photo.png")).toBe("image");
|
|
});
|
|
|
|
it("falls back to the name when the type is a generic wrapper", () => {
|
|
// What an upload gets when the browser cannot guess -- files.ts stores
|
|
// `f.type || "application/octet-stream"`, so this is the common case for
|
|
// anything unusual, and it is what made the old exact-type check useless
|
|
// on real uploads.
|
|
expect(previewKind("application/octet-stream", "README.md")).toBe("text");
|
|
expect(previewKind("application/octet-stream", "shot.PNG")).toBe("image");
|
|
expect(previewKind("application/octet-stream", "report.pdf")).toBe("pdf");
|
|
expect(previewKind("", "notes.txt")).toBe("text");
|
|
expect(previewKind(null, "deploy.sh")).toBe("text");
|
|
expect(previewKind(undefined, undefined)).toBeNull();
|
|
});
|
|
|
|
it("does not let the name override a type the server was specific about", () => {
|
|
// A .txt served as a zip is a zip. Guessing from the name here would be
|
|
// taking the sender's word for the extension over the server's for the
|
|
// bytes.
|
|
expect(previewKind("application/zip", "archive.txt")).toBeNull();
|
|
expect(previewKind("video/mp4", "clip.txt")).toBeNull();
|
|
});
|
|
|
|
it("leaves SVG alone", () => {
|
|
// It carries script and the server refuses to serve it inline; it stays a
|
|
// download until that is decided deliberately.
|
|
expect(previewKind("image/svg+xml", "logo.svg")).toBeNull();
|
|
expect(previewKind("application/octet-stream", "logo.svg")).toBeNull();
|
|
});
|
|
|
|
it("has nothing to show for the rest", () => {
|
|
expect(previewKind("application/zip", "backup.zip")).toBeNull();
|
|
expect(previewKind("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "letter.docx")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("openableInTab", () => {
|
|
/*
|
|
* This mirrors `isInlineSafe` in server/src/app.ts. If the two drift, the
|
|
* "open in a new tab" button silently starts downloading instead, because
|
|
* the server sends Content-Disposition: attachment for anything not on its
|
|
* list. These cases are the list.
|
|
*/
|
|
it("matches what the server will serve inline", () => {
|
|
expect(openableInTab("image/png")).toBe(true);
|
|
expect(openableInTab("video/mp4")).toBe(true);
|
|
expect(openableInTab("audio/mpeg")).toBe(true);
|
|
expect(openableInTab("application/pdf")).toBe(true);
|
|
expect(openableInTab("text/plain; charset=utf-8")).toBe(true);
|
|
expect(openableInTab("text/calendar")).toBe(true);
|
|
expect(openableInTab("text/vcard")).toBe(true);
|
|
});
|
|
|
|
it("refuses what the server will not", () => {
|
|
expect(openableInTab("image/svg+xml")).toBe(false);
|
|
expect(openableInTab("text/html")).toBe(false);
|
|
expect(openableInTab("text/markdown")).toBe(false);
|
|
expect(openableInTab("application/json")).toBe(false);
|
|
expect(openableInTab("application/octet-stream")).toBe(false);
|
|
expect(openableInTab(null)).toBe(false);
|
|
});
|
|
|
|
it("is narrower than what we can show ourselves", () => {
|
|
// Markdown is the case that proves the two questions are different: the
|
|
// dialog reads it with fetch, which ignores Content-Disposition.
|
|
expect(previewKind("text/markdown", "README.md")).toBe("text");
|
|
expect(openableInTab("text/markdown")).toBe(false);
|
|
});
|
|
});
|