You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

263 lines
7.5 KiB

import { describe, expect, test } from "bun:test";
import {
buildMarkdownExportFileName,
downloadMarkdownFile,
normalizeMarkdownImageUrls,
} from "./markdown-export";
describe("normalizeMarkdownImageUrls", () => {
test("converts /public/upload image links to absolute URLs", () => {
const markdown = "![cover](/public/upload/posts/cover.png)";
const result = normalizeMarkdownImageUrls(markdown, "https://example.com");
expect(result).toBe("![cover](https://example.com/public/upload/posts/cover.png)");
});
test("converts other site-relative image links to absolute URLs", () => {
const markdown = "![hero](/images/a.png)";
const result = normalizeMarkdownImageUrls(markdown, "https://example.com");
expect(result).toBe("![hero](https://example.com/images/a.png)");
});
test("keeps absolute http/https image links unchanged", () => {
const markdown = [
"![a](http://cdn.example.com/a.png)",
"![b](https://cdn.example.com/b.png)",
].join("\n");
const result = normalizeMarkdownImageUrls(markdown, "https://example.com");
expect(result).toBe(markdown);
});
test("keeps protocol-relative and data URI image links unchanged", () => {
const markdown = [
"![protocol](//cdn.example.com/p.png)",
"![inline](data:image/png;base64,AAAB)",
].join("\n");
const result = normalizeMarkdownImageUrls(markdown, "https://example.com");
expect(result).toBe(markdown);
});
test("does not change normal markdown links", () => {
const markdown = "[read more](/public/upload/posts/cover.png)";
const result = normalizeMarkdownImageUrls(markdown, "https://example.com");
expect(result).toBe(markdown);
});
});
describe("buildMarkdownExportFileName", () => {
test("prefers slug when slug is not empty", () => {
const result = buildMarkdownExportFileName({ slug: "hello-world", id: 42 });
expect(result).toBe("hello-world.md");
});
test("falls back to post-<id>.md when slug is empty", () => {
const result = buildMarkdownExportFileName({ slug: "", id: 42 });
expect(result).toBe("post-42.md");
});
});
describe("downloadMarkdownFile", () => {
test("does not throw when window/document are unavailable", () => {
const windowDescriptor = Object.getOwnPropertyDescriptor(globalThis, "window");
const documentDescriptor = Object.getOwnPropertyDescriptor(globalThis, "document");
Object.defineProperty(globalThis, "window", {
value: undefined,
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, "document", {
value: undefined,
configurable: true,
writable: true,
});
try {
expect(() => downloadMarkdownFile("post.md", "# hello")).not.toThrow();
} finally {
if (windowDescriptor) {
Object.defineProperty(globalThis, "window", windowDescriptor);
} else {
Reflect.deleteProperty(globalThis, "window");
}
if (documentDescriptor) {
Object.defineProperty(globalThis, "document", documentDescriptor);
} else {
Reflect.deleteProperty(globalThis, "document");
}
}
});
test("creates object URL, clicks anchor, and revokes URL in browser flow", () => {
const windowDescriptor = Object.getOwnPropertyDescriptor(globalThis, "window");
const documentDescriptor = Object.getOwnPropertyDescriptor(globalThis, "document");
const urlDescriptor = Object.getOwnPropertyDescriptor(globalThis, "URL");
let createElementArg = "";
let appendedNode: unknown;
let clicked = false;
let removed = false;
let revokedArg = "";
const blobUrl = "blob:test-url";
const anchor = {
href: "",
download: "",
style: { display: "" },
click: () => {
clicked = true;
},
remove: () => {
removed = true;
},
};
Object.defineProperty(globalThis, "window", {
value: {},
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, "document", {
value: {
createElement: (tag: string) => {
createElementArg = tag;
return anchor;
},
body: {
appendChild: (node: unknown) => {
appendedNode = node;
},
},
},
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, "URL", {
value: {
createObjectURL: (_blob: Blob) => blobUrl,
revokeObjectURL: (url: string) => {
revokedArg = url;
},
},
configurable: true,
writable: true,
});
try {
downloadMarkdownFile("hello.md", "# hello");
expect(createElementArg).toBe("a");
expect(appendedNode).toBe(anchor);
expect(anchor.href).toBe(blobUrl);
expect(anchor.download).toBe("hello.md");
expect(anchor.style.display).toBe("none");
expect(clicked).toBe(true);
expect(removed).toBe(true);
expect(revokedArg).toBe(blobUrl);
} finally {
if (windowDescriptor) {
Object.defineProperty(globalThis, "window", windowDescriptor);
} else {
Reflect.deleteProperty(globalThis, "window");
}
if (documentDescriptor) {
Object.defineProperty(globalThis, "document", documentDescriptor);
} else {
Reflect.deleteProperty(globalThis, "document");
}
if (urlDescriptor) {
Object.defineProperty(globalThis, "URL", urlDescriptor);
} else {
Reflect.deleteProperty(globalThis, "URL");
}
}
});
test("still revokes object URL when anchor click throws", () => {
const windowDescriptor = Object.getOwnPropertyDescriptor(globalThis, "window");
const documentDescriptor = Object.getOwnPropertyDescriptor(globalThis, "document");
const urlDescriptor = Object.getOwnPropertyDescriptor(globalThis, "URL");
let revokedArg = "";
let removed = false;
const blobUrl = "blob:error-case";
const clickError = new Error("click failed");
const anchor = {
href: "",
download: "",
style: { display: "" },
click: () => {
throw clickError;
},
remove: () => {
removed = true;
},
};
Object.defineProperty(globalThis, "window", {
value: {},
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, "document", {
value: {
createElement: (_tag: string) => anchor,
body: {
appendChild: (_node: unknown) => undefined,
},
},
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, "URL", {
value: {
createObjectURL: (_blob: Blob) => blobUrl,
revokeObjectURL: (url: string) => {
revokedArg = url;
},
},
configurable: true,
writable: true,
});
try {
expect(() => downloadMarkdownFile("broken.md", "# broken")).toThrow("click failed");
expect(removed).toBe(true);
expect(revokedArg).toBe(blobUrl);
} finally {
if (windowDescriptor) {
Object.defineProperty(globalThis, "window", windowDescriptor);
} else {
Reflect.deleteProperty(globalThis, "window");
}
if (documentDescriptor) {
Object.defineProperty(globalThis, "document", documentDescriptor);
} else {
Reflect.deleteProperty(globalThis, "document");
}
if (urlDescriptor) {
Object.defineProperty(globalThis, "URL", urlDescriptor);
} else {
Reflect.deleteProperty(globalThis, "URL");
}
}
});
});