Add tests for the main process using Vitest

Add tests for FileLibrary
This commit is contained in:
Jonatan Heyman 2026-01-18 18:20:40 +01:00
parent 2e1b7e9a41
commit 6b41281c9f
5 changed files with 1611 additions and 0 deletions

View File

@ -16,6 +16,8 @@ jobs:
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test
- name: Run Main Process tests
run: npm run test:main
- uses: actions/upload-artifact@v4
if: always()
with:

1403
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -29,6 +29,7 @@
"webapp:build": "vite build webapp",
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:main": "vitest",
"postinstall": "patch-package"
},
"devDependencies": {
@ -80,6 +81,7 @@
"vite": "^6.3.5",
"vite-plugin-electron": "^0.11.1",
"vite-plugin-electron-renderer": "^0.11.4",
"vitest": "^2.1.9",
"vue": "^3.5.13",
"vue-tsc": "^2.2.10",
"vuedraggable": "^4.1.0"

View File

@ -0,0 +1,188 @@
import fs from "node:fs"
import os from "node:os"
import path from "node:path"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { SCRATCH_FILE_NAME } from "../../src/common/constants.js"
vi.mock("electron", () => ({
app: { quit: vi.fn(), getPath: vi.fn() },
ipcMain: { handle: vi.fn() },
dialog: { showOpenDialog: vi.fn() },
}))
vi.mock("../../electron/config.js", () => ({
default: { get: vi.fn(() => "") },
}))
const getImgReferencesMock = vi.fn(async () => [])
const loadFileLibrary = async ({ mockRipgrep = false } = {}) => {
vi.resetModules()
if (mockRipgrep) {
vi.doMock("../../electron/main/ripgrep.js", () => ({
searchLibrary: vi.fn(),
getImgReferences: (...args) => getImgReferencesMock(...args),
}))
} else {
vi.unmock("../../electron/main/ripgrep.js")
}
return await import("../../electron/main/file-library.js")
}
const makeTempDir = () =>
fs.mkdtempSync(path.join(os.tmpdir(), "heynote-file-library-"))
describe("FileLibrary", () => {
let tmpDir = ""
let library = null
beforeEach(() => {
tmpDir = makeTempDir()
getImgReferencesMock.mockClear()
})
afterEach(() => {
if (library) {
library.close()
library = null
}
fs.rmSync(tmpDir, { recursive: true, force: true })
})
it("creates the scratch file if missing", async () => {
const { FileLibrary } = await loadFileLibrary()
library = new FileLibrary(tmpDir, {})
const scratchPath = path.join(tmpDir, SCRATCH_FILE_NAME)
expect(fs.existsSync(scratchPath)).toBe(true)
})
it("creates, loads, and saves notes", async () => {
const { FileLibrary } = await loadFileLibrary()
library = new FileLibrary(tmpDir, {})
await library.create("note.txt", "hello")
const content = await library.load("note.txt")
expect(content).toBe("hello")
await library.save("note.txt", "updated")
const updated = fs.readFileSync(path.join(tmpDir, "note.txt"), "utf8")
expect(updated).toBe("updated")
})
it("moves and deletes notes", async () => {
const { FileLibrary } = await loadFileLibrary()
library = new FileLibrary(tmpDir, {})
await library.create("note.txt", "content")
await library.move("note.txt", "moved.txt")
expect(fs.existsSync(path.join(tmpDir, "note.txt"))).toBe(false)
expect(fs.existsSync(path.join(tmpDir, "moved.txt"))).toBe(true)
await library.delete("moved.txt")
expect(fs.existsSync(path.join(tmpDir, "moved.txt"))).toBe(false)
})
it("rejects deleting the scratch file", async () => {
const { FileLibrary } = await loadFileLibrary()
library = new FileLibrary(tmpDir, {})
await expect(library.delete(SCRATCH_FILE_NAME)).rejects.toThrow(
"Can't delete scratch file"
)
})
it("returns metadata from getList", async () => {
const { FileLibrary } = await loadFileLibrary()
library = new FileLibrary(tmpDir, {})
const metadata = { name: "Test Note", tags: ["a", "b"] }
const note = `${JSON.stringify(metadata)}\n∞∞∞text\nbody`
await fs.promises.writeFile(path.join(tmpDir, "meta.txt"), note, "utf8")
await fs.promises.writeFile(
path.join(tmpDir, "plain.txt"),
"no metadata",
"utf8"
)
const list = await library.getList()
expect(list["meta.txt"]).toEqual(metadata)
expect(list["plain.txt"]).toBeNull()
})
it("detects changes with NoteBuffer.loadIfChanged", async () => {
const { FileLibrary, NoteBuffer } = await loadFileLibrary()
library = new FileLibrary(tmpDir, {})
const fullPath = path.join(tmpDir, "change.txt")
await fs.promises.writeFile(fullPath, "first", "utf8")
const buffer = new NoteBuffer({ fullPath, library })
await buffer.load()
expect(await buffer.loadIfChanged()).toBeNull()
await fs.promises.writeFile(fullPath, "second", "utf8")
expect(await buffer.loadIfChanged()).toBe("second")
})
it("removes unreferenced images older than a day when references exist", async () => {
const { FileLibrary } = await loadFileLibrary()
const imagesDir = path.join(tmpDir, ".images")
await fs.promises.mkdir(imagesDir)
const referencedImage = "keep.png"
const orphanImage = "delete.png"
await fs.promises.writeFile(path.join(imagesDir, referencedImage), "")
await fs.promises.writeFile(path.join(imagesDir, orphanImage), "")
const noteContent = `<∞img;id=1;file=heynote-file://image/${referencedImage};w=1;h=1∞>`
await fs.promises.writeFile(path.join(tmpDir, "note.txt"), noteContent)
library = new FileLibrary(tmpDir, {})
const oldTime = new Date(Date.now() - 1000 * 60 * 60 * 25)
await fs.promises.utimes(
path.join(imagesDir, orphanImage),
oldTime,
oldTime
)
await library.removeUnreferencedImages()
expect(fs.existsSync(path.join(imagesDir, referencedImage))).toBe(true)
expect(fs.existsSync(path.join(imagesDir, orphanImage))).toBe(false)
})
it("keeps unreferenced images when no references exist", async () => {
const { FileLibrary } = await loadFileLibrary()
const imagesDir = path.join(tmpDir, ".images")
await fs.promises.mkdir(imagesDir)
const orphanImage = "keep.png"
await fs.promises.writeFile(path.join(imagesDir, orphanImage), "")
const oldTime = new Date(Date.now() - 1000 * 60 * 60 * 25)
await fs.promises.utimes(
path.join(imagesDir, orphanImage),
oldTime,
oldTime
)
library = new FileLibrary(tmpDir, {})
await library.removeUnreferencedImages()
expect(fs.existsSync(path.join(imagesDir, orphanImage))).toBe(true)
})
it("keeps images when ripgrep fails", async () => {
const { FileLibrary } = await loadFileLibrary({ mockRipgrep: true })
const imagesDir = path.join(tmpDir, ".images")
await fs.promises.mkdir(imagesDir)
const orphanImage = "keep.png"
await fs.promises.writeFile(path.join(imagesDir, orphanImage), "")
getImgReferencesMock.mockRejectedValueOnce(new Error("rg failed"))
library = new FileLibrary(tmpDir, {})
await library.removeUnreferencedImages()
expect(fs.existsSync(path.join(imagesDir, orphanImage))).toBe(true)
})
})

16
vitest.config.mjs Normal file
View File

@ -0,0 +1,16 @@
import path from "node:path"
import { fileURLToPath } from "node:url"
import { defineConfig } from "vitest/config"
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(path.dirname(fileURLToPath(import.meta.url))),
},
},
test: {
environment: "node",
include: ["tests/main/**/*.test.js"],
exclude: ["tests/**/*.spec.js", "tests/**/*.spec.ts"],
},
})