Add Vitest and unit tests for src/lib/ (#242)

* Add Vitest and unit tests for src/lib/ utilities

Closes #241

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update node types

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
tmshvandClaude Opus 4.6 authored and GitHub committed 2026-02-08 02:32:06 +07:00
1 parent a5bd82eb3c
commit f5a95f57c8
11 files changed
+1686 -48

No files matched your search

+1415 -42
View File
File diff suppressed because it is too large. Load diff
+8 -6
View File
@@ -11,7 +11,8 @@
"lint": "eslint .", "lint": "eslint .",
"format": "eslint --fix .", "format": "eslint --fix .",
"sitemap": "node scripts/sitemap.js", "sitemap": "node scripts/sitemap.js",
"feed": "node scripts/feed.js" "feed": "node scripts/feed.js",
"test": "vitest run"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -40,17 +41,18 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
"@types/classnames": "^2.2.10", "@types/classnames": "^2.2.10",
"@types/markdown-it": "^12.2.3", "@types/markdown-it": "^12.2.3",
"@types/node": "^18.13.0", "@types/node": "^22.19.9",
"@types/react": "^18.0.18", "@types/react": "^18.0.18",
"@types/sharp": "^0.29.2", "@types/sharp": "^0.29.2",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
"eslint": "^9.0.0",
"eslint-config-next": "^15.5.0", "eslint-config-next": "^15.5.0",
"feed": "^4.1.0", "feed": "^4.1.0",
"eslint": "^9.0.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"typescript": "^5.9.0" "typescript": "^5.9.0",
"vitest": "^4.0.18"
} }
} }
+37
View File
@@ -0,0 +1,37 @@
import { describe, it, expect } from "vitest"
import { insertBetween, tail } from "./array"
describe("insertBetween", () => {
it("should return empty array for empty input", () => {
expect(insertBetween([], () => ",")).toEqual([])
})
it("should return single item without separator", () => {
expect(insertBetween(["a"], () => ",")).toEqual(["a"])
})
it("should insert separators between items", () => {
expect(insertBetween(["a", "b", "c"], () => ",")).toEqual([
"a", ",", "b", ",", "c",
])
})
it("should pass index to separator function", () => {
const result = insertBetween(["a", "b", "c"], (i) => i)
expect(result).toEqual(["a", 0, "b", 1, "c"])
})
})
describe("tail", () => {
it("should return empty array for empty input", () => {
expect(tail([])).toEqual([])
})
it("should return empty array for single item", () => {
expect(tail(["a"])).toEqual([])
})
it("should return all items except first", () => {
expect(tail([1, 2, 3])).toEqual([2, 3])
})
})
+14
View File
@@ -0,0 +1,14 @@
import { describe, it, expect } from "vitest"
import { dateFormat } from "./date"
describe("dateFormat", () => {
it("should format a Date object in Russian locale", () => {
const result = dateFormat(new Date(2024, 0, 15))
expect(result).toBe("15 января 2024")
})
it("should format a date string in Russian locale", () => {
const result = dateFormat("2024-06-01T00:00:00.000Z")
expect(result).toBe("1 июня 2024")
})
})
+50
View File
@@ -0,0 +1,50 @@
import { describe, it, expect } from "vitest"
import { ext, size } from "./file"
describe("ext", () => {
it("should return ПДФ for application/pdf", () => {
expect(ext("application/pdf")).toBe("ПДФ")
})
it("should return ЖПГ for image/jpeg", () => {
expect(ext("image/jpeg")).toBe("ЖПГ")
})
it("should return ПНГ for image/png", () => {
expect(ext("image/png")).toBe("ПНГ")
})
it("should return raw MIME type for unknown types", () => {
expect(ext("text/plain")).toBe("text/plain")
})
})
describe("size", () => {
it("should return empty string for NaN", () => {
expect(size(NaN)).toBe("")
})
it("should return empty string for non-finite values", () => {
expect(size(Infinity)).toBe("")
})
it("should format bytes", () => {
expect(size(500)).toBe("500.0 байт")
})
it("should format kilobytes", () => {
expect(size(1024)).toBe("1.0 КБ")
})
it("should format megabytes", () => {
expect(size(1024 * 1024)).toBe("1.0 МБ")
})
it("should format gigabytes", () => {
expect(size(1024 * 1024 * 1024)).toBe("1.0 ГБ")
})
it("should respect precision parameter", () => {
expect(size(1536, 2)).toBe("1.50 КБ")
})
})
+25
View File
@@ -0,0 +1,25 @@
import { describe, it, expect } from "vitest"
import { getResizedUrl } from "./image"
describe("getResizedUrl", () => {
it("should construct resize URL with default n", () => {
const result = getResizedUrl("example.com/img.jpg", {
width: 800,
height: 600,
})
expect(result).toBe(
"https://images.weserv.nl/?url=example.com/img.jpg&w=800&h=600&n=1",
)
})
it("should use custom n parameter", () => {
const result = getResizedUrl("example.com/img.jpg", {
width: 400,
height: 300,
n: -1,
})
expect(result).toBe(
"https://images.weserv.nl/?url=example.com/img.jpg&w=400&h=300&n=-1",
)
})
})
+61
View File
@@ -0,0 +1,61 @@
import { describe, it, expect } from "vitest"
import { meta, MetaBuilder } from "./meta"
describe("meta", () => {
it("should return defaults when called with empty object", () => {
const result = meta({})
expect(result.title).toBe("Шлиссельбургская ДХШ")
expect(result.description).toBe("Сайт Шлиссельбургской художественной школы")
expect(result.url).toBe("https://art.shlisselburg.org/")
expect(result.locale).toBe("ru_RU")
})
it("should override defaults with provided values", () => {
const result = meta({ title: "Custom Title", url: "/about" })
expect(result.title).toBe("Custom Title")
expect(result.url).toBe("https://art.shlisselburg.org/about")
})
it("should construct URL from path", () => {
const result = meta({ url: "/news/2024" })
expect(result.url).toBe("https://art.shlisselburg.org/news/2024")
})
})
describe("MetaBuilder", () => {
it("should support method chaining", () => {
const builder = new MetaBuilder()
const result = builder
.setTitle("Test")
.setDescription("Desc")
.build()
expect(result.title).toBe("Test")
expect(result.description).toBe("Desc")
})
it("should use default meta values", () => {
const result = new MetaBuilder().build()
expect(result.locale).toBe("ru_RU")
expect(result.domain).toBe("art.shlisselburg.org")
})
it("should set image from Pic object", () => {
const result = new MetaBuilder()
.setImage({ src: "http://example.com/img.jpg", width: 800, height: 600 })
.build()
expect(result.image).toBe("http://example.com/img.jpg")
expect(result.imageWidth).toBe(800)
expect(result.imageHeight).toBe(600)
})
it("should apply data from setData", () => {
const result = new MetaBuilder()
.setData({ url: "/test", type: "article" })
.build()
expect(result.url).toBe("https://art.shlisselburg.org/test")
expect(result.type).toBe("article")
})
})
+20
View File
@@ -0,0 +1,20 @@
import { describe, it, expect } from "vitest"
import { titleCase } from "./string"
describe("titleCase", () => {
it("should capitalize first letter of lowercase string", () => {
expect(titleCase("hello")).toBe("Hello")
})
it("should keep already capitalized string", () => {
expect(titleCase("Hello")).toBe("Hello")
})
it("should handle empty string", () => {
expect(titleCase("")).toBe("")
})
it("should capitalize single character", () => {
expect(titleCase("a")).toBe("A")
})
})
+24
View File
@@ -0,0 +1,24 @@
import { describe, it, expect } from "vitest"
import { markdownToHtml, typograf } from "./text"
describe("markdownToHtml", () => {
it("should convert markdown paragraph to HTML", () => {
expect(markdownToHtml("hello")).toBe("<p>hello</p>\n")
})
it("should convert bold text", () => {
expect(markdownToHtml("**bold**")).toBe("<p><strong>bold</strong></p>\n")
})
it("should pass through raw HTML", () => {
expect(markdownToHtml("<div>test</div>")).toBe("<div>test</div>")
})
})
describe("typograf", () => {
it("should apply Russian typographic rules", () => {
const result = typograf("Привет - мир")
expect(result).toContain("Привет")
expect(result).toContain("мир")
})
})
+18
View File
@@ -0,0 +1,18 @@
import { describe, it, expect } from "vitest"
import { queryList } from "./url"
describe("queryList", () => {
it("should return empty string for empty values", () => {
expect(queryList("tag", [])).toBe("")
})
it("should return single key=value pair", () => {
expect(queryList("tag", ["art"])).toBe("tag=art")
})
it("should join multiple values with &", () => {
expect(queryList("tag", ["art", "music", "photo"])).toBe(
"tag=art&tag=music&tag=photo",
)
})
})
+14
View File
@@ -0,0 +1,14 @@
import { defineConfig } from "vitest/config"
import path from "path"
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
src: path.resolve(__dirname, "src"),
},
},
test: {
include: ["src/**/*.test.ts"],
},
})