These fonts apply to markdown headings (h1-h6), so "heading" is more accurate than "title". Renames the chosen*Font field, store action, and the --heading-font-family CSS variable. Keeps backward compatibility: .remanso.json still honors the legacy `t` key (alongside the new `h`), and a saved `chosenTitleFont` in localStorage is migrated on load so existing preferences survive.
224 lines
6.6 KiB
TypeScript
224 lines
6.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
|
|
const { reposGet } = vi.hoisted(() => ({ reposGet: vi.fn() }))
|
|
|
|
vi.mock("./octo", () => ({
|
|
getOctokit: vi.fn().mockResolvedValue({
|
|
repos: { get: reposGet }
|
|
}),
|
|
runWithAuthRetry: vi.fn()
|
|
}))
|
|
|
|
// Stub heavyweight transitive imports so loading repo.ts doesn't spin up the
|
|
// data Web Worker or the full markdown rendering stack.
|
|
vi.mock("@/hooks/useMarkdown.hook", () => ({
|
|
markdownBuilder: () => ({ render: (s: string) => s })
|
|
}))
|
|
vi.mock("@/modules/note/cache/prepareNoteCache", () => ({
|
|
prepareNoteCache: () => ({
|
|
getCachedNote: async () => ({ note: null }),
|
|
saveCacheNote: vi.fn()
|
|
})
|
|
}))
|
|
|
|
import { getOctokit, runWithAuthRetry } from "./octo"
|
|
import {
|
|
getFiles,
|
|
getRepoPermission,
|
|
getUserSettingsContent,
|
|
queryFileContent
|
|
} from "./repo"
|
|
|
|
const makeOctokitWithRequest = (impl: (route: string, params: unknown) => unknown) => ({
|
|
request: vi.fn(impl)
|
|
})
|
|
|
|
describe("getRepoPermission", () => {
|
|
beforeEach(() => {
|
|
reposGet.mockReset()
|
|
vi.mocked(getOctokit).mockResolvedValue({
|
|
repos: { get: reposGet }
|
|
} as never)
|
|
})
|
|
|
|
it("returns true when permissions.push is true", async () => {
|
|
reposGet.mockResolvedValue({
|
|
data: { permissions: { push: true } }
|
|
})
|
|
await expect(getRepoPermission("owner", "repo")).resolves.toBe(true)
|
|
})
|
|
|
|
it("returns false when permissions.push is false", async () => {
|
|
reposGet.mockResolvedValue({
|
|
data: { permissions: { push: false } }
|
|
})
|
|
await expect(getRepoPermission("owner", "repo")).resolves.toBe(false)
|
|
})
|
|
|
|
it("returns false when permissions is missing (anonymous request)", async () => {
|
|
reposGet.mockResolvedValue({
|
|
data: {}
|
|
})
|
|
await expect(getRepoPermission("owner", "repo")).resolves.toBe(false)
|
|
})
|
|
|
|
it("returns false when owner or repo is empty", async () => {
|
|
await expect(getRepoPermission("", "repo")).resolves.toBe(false)
|
|
await expect(getRepoPermission("owner", "")).resolves.toBe(false)
|
|
expect(reposGet).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe("getFiles", () => {
|
|
beforeEach(() => {
|
|
vi.mocked(getOctokit).mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it("returns empty array when owner is missing", async () => {
|
|
expect(await getFiles("", "repo")).toEqual([])
|
|
expect(getOctokit).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it("returns empty array when repo is missing", async () => {
|
|
expect(await getFiles("owner", "")).toEqual([])
|
|
expect(getOctokit).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it("returns empty array when there are no commits", async () => {
|
|
const octokit = makeOctokitWithRequest((route) => {
|
|
if (route === "GET /repos/{owner}/{repo}/commits") {
|
|
return { data: [] }
|
|
}
|
|
throw new Error("unexpected route " + route)
|
|
})
|
|
vi.mocked(getOctokit).mockResolvedValue(octokit as never)
|
|
|
|
expect(await getFiles("owner", "repo")).toEqual([])
|
|
})
|
|
|
|
it("fetches the tree from the latest commit and filters out non-blob entries", async () => {
|
|
const octokit = makeOctokitWithRequest((route, params) => {
|
|
if (route === "GET /repos/{owner}/{repo}/commits") {
|
|
return {
|
|
data: [{ commit: { tree: { sha: "TREE_SHA" } } }]
|
|
}
|
|
}
|
|
if (route === "GET /repos/{owner}/{repo}/git/trees/{tree_sha}") {
|
|
expect((params as { tree_sha: string }).tree_sha).toBe("TREE_SHA")
|
|
expect((params as { recursive: string }).recursive).toBe("true")
|
|
return {
|
|
data: {
|
|
tree: [
|
|
{ path: "README.md", type: "blob", sha: "a" },
|
|
{ path: "src", type: "tree", sha: "b" },
|
|
{ path: "src/note.md", type: "blob", sha: "c" }
|
|
]
|
|
}
|
|
}
|
|
}
|
|
throw new Error("unexpected route " + route)
|
|
})
|
|
vi.mocked(getOctokit).mockResolvedValue(octokit as never)
|
|
|
|
const files = await getFiles("owner", "repo")
|
|
|
|
expect(files.map((f) => f.path)).toEqual(["README.md", "src/note.md"])
|
|
})
|
|
})
|
|
|
|
describe("queryFileContent", () => {
|
|
beforeEach(() => {
|
|
vi.mocked(runWithAuthRetry).mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it("returns null when owner or repo is missing", async () => {
|
|
expect(await queryFileContent("", "repo", "sha")).toBeNull()
|
|
expect(await queryFileContent("owner", "", "sha")).toBeNull()
|
|
expect(runWithAuthRetry).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it("returns the blob content via runWithAuthRetry", async () => {
|
|
vi.mocked(runWithAuthRetry).mockImplementation(async (call) => {
|
|
const octokit = {
|
|
request: vi.fn().mockResolvedValue({
|
|
data: { content: "BASE64" }
|
|
})
|
|
}
|
|
return call(octokit as never)
|
|
})
|
|
|
|
expect(await queryFileContent("owner", "repo", "SHA")).toBe("BASE64")
|
|
})
|
|
|
|
it("returns null and swallows errors when the call fails", async () => {
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
vi.mocked(runWithAuthRetry).mockRejectedValue(new Error("boom"))
|
|
|
|
expect(await queryFileContent("owner", "repo", "SHA")).toBeNull()
|
|
expect(warn).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe("getUserSettingsContent", () => {
|
|
beforeEach(() => {
|
|
vi.mocked(runWithAuthRetry).mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
const configFiles = [
|
|
{ path: ".remanso.json", type: "blob", sha: "CFG" }
|
|
] as never
|
|
|
|
const withConfig = (json: object) => {
|
|
const base64 = btoa(JSON.stringify(json))
|
|
vi.mocked(runWithAuthRetry).mockImplementation(async (call) => {
|
|
const octokit = {
|
|
request: vi.fn().mockResolvedValue({ data: { content: base64 } })
|
|
}
|
|
return call(octokit as never)
|
|
})
|
|
}
|
|
|
|
it("maps the `h` key to chosenHeadingFont and `p` to chosenBodyFont", async () => {
|
|
withConfig({ h: "Lora", p: "Inter" })
|
|
|
|
const settings = await getUserSettingsContent("owner", "repo", configFiles)
|
|
|
|
expect(settings?.chosenHeadingFont).toBe("Lora")
|
|
expect(settings?.chosenBodyFont).toBe("Inter")
|
|
})
|
|
|
|
it("falls back to the legacy `t` key for chosenHeadingFont", async () => {
|
|
withConfig({ t: "Merriweather", p: "Inter" })
|
|
|
|
const settings = await getUserSettingsContent("owner", "repo", configFiles)
|
|
|
|
expect(settings?.chosenHeadingFont).toBe("Merriweather")
|
|
})
|
|
|
|
it("prefers `h` over the legacy `t` when both are present", async () => {
|
|
withConfig({ h: "Lora", t: "Merriweather" })
|
|
|
|
const settings = await getUserSettingsContent("owner", "repo", configFiles)
|
|
|
|
expect(settings?.chosenHeadingFont).toBe("Lora")
|
|
})
|
|
|
|
it("returns null when there is no .remanso.json", async () => {
|
|
expect(
|
|
await getUserSettingsContent("owner", "repo", [] as never)
|
|
).toBeNull()
|
|
})
|
|
})
|