feat(testing): implement vitest test suite

Add comprehensive testing infrastructure with vitest, React Testing
Library, and jsdom. Includes tests for:

- Utility functions (cn, isChromeBrowser, date formatting, JSON utils)
- Variable detection module (7 pattern types, conversion, false positives)
- API routes (health check, user registration with validation/auth)

127 tests passing covering critical functionality.
This commit is contained in:
Claude
2026-01-06 18:52:06 +00:00
parent b052f0d05d
commit 0ce1a0e35d
10 changed files with 4166 additions and 3 deletions

2886
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,11 @@
"db:setup": "prisma generate && prisma migrate dev && prisma db seed",
"setup": "node scripts/setup.js",
"generate:examples": "npx tsx scripts/generate-examples.ts",
"postinstall": "prisma generate"
"postinstall": "prisma generate",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@auth/prisma-adapter": "^2.11.1",
@@ -73,19 +77,27 @@
"devDependencies": {
"@clack/prompts": "^0.11.0",
"@tailwindcss/postcss": "^4",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/bcryptjs": "^2.4.6",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8",
"babel-plugin-react-compiler": "1.0.0",
"eslint": "^9",
"eslint-config-next": "16.0.7",
"jsdom": "^25.0.1",
"picocolors": "^1.1.1",
"prisma": "^6.19.0",
"tailwindcss": "^4",
"tsx": "^4.21.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
"typescript": "^5",
"vitest": "^2.1.8"
},
"engines": {
"node": "24.x"

View File

@@ -0,0 +1,66 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { GET } from "@/app/api/health/route";
import { db } from "@/lib/db";
// Mock the db module
vi.mock("@/lib/db", () => ({
db: {
$queryRaw: vi.fn(),
},
}));
describe("GET /api/health", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should return healthy status when database is connected", async () => {
// Mock successful database query
vi.mocked(db.$queryRaw).mockResolvedValueOnce([{ "?column?": 1 }]);
const response = await GET();
const data = await response.json();
expect(response.status).toBe(200);
expect(data.status).toBe("healthy");
expect(data.database).toBe("connected");
expect(data.timestamp).toBeDefined();
});
it("should return unhealthy status when database is disconnected", async () => {
// Mock database error
vi.mocked(db.$queryRaw).mockRejectedValueOnce(new Error("Connection failed"));
const response = await GET();
const data = await response.json();
expect(response.status).toBe(503);
expect(data.status).toBe("unhealthy");
expect(data.database).toBe("disconnected");
expect(data.error).toBe("Connection failed");
expect(data.timestamp).toBeDefined();
});
it("should handle unknown error type", async () => {
// Mock non-Error rejection
vi.mocked(db.$queryRaw).mockRejectedValueOnce("Unknown error");
const response = await GET();
const data = await response.json();
expect(response.status).toBe(503);
expect(data.status).toBe("unhealthy");
expect(data.error).toBe("Unknown error");
});
it("should include ISO timestamp in response", async () => {
vi.mocked(db.$queryRaw).mockResolvedValueOnce([{ "?column?": 1 }]);
const response = await GET();
const data = await response.json();
// Verify timestamp is valid ISO format
const timestamp = new Date(data.timestamp);
expect(timestamp.toISOString()).toBe(data.timestamp);
});
});

View File

@@ -0,0 +1,306 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { POST } from "@/app/api/auth/register/route";
import { db } from "@/lib/db";
import { getConfig } from "@/lib/config";
// Mock dependencies
vi.mock("@/lib/db", () => ({
db: {
user: {
findUnique: vi.fn(),
create: vi.fn(),
},
},
}));
vi.mock("@/lib/config", () => ({
getConfig: vi.fn(),
}));
vi.mock("bcryptjs", () => ({
default: {
hash: vi.fn().mockResolvedValue("hashed_password"),
},
}));
function createRequest(body: object): Request {
return new Request("http://localhost:3000/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
describe("POST /api/auth/register", () => {
beforeEach(() => {
vi.resetAllMocks();
// Default: registration is enabled
vi.mocked(getConfig).mockResolvedValue({
auth: { allowRegistration: true, providers: [] },
features: {},
});
// Default: no existing users
vi.mocked(db.user.findUnique).mockResolvedValue(null);
});
describe("validation", () => {
it("should return 400 for missing name", async () => {
const request = createRequest({
username: "testuser",
email: "test@example.com",
password: "password123",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("validation_error");
});
it("should return 400 for name too short", async () => {
const request = createRequest({
name: "A",
username: "testuser",
email: "test@example.com",
password: "password123",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("validation_error");
});
it("should return 400 for missing username", async () => {
const request = createRequest({
name: "Test User",
email: "test@example.com",
password: "password123",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("validation_error");
});
it("should return 400 for invalid username format", async () => {
const request = createRequest({
name: "Test User",
username: "invalid-username!",
email: "test@example.com",
password: "password123",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("validation_error");
});
it("should return 400 for invalid email", async () => {
const request = createRequest({
name: "Test User",
username: "testuser",
email: "invalid-email",
password: "password123",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("validation_error");
});
it("should return 400 for password too short", async () => {
const request = createRequest({
name: "Test User",
username: "testuser",
email: "test@example.com",
password: "12345",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("validation_error");
});
});
describe("registration disabled", () => {
it("should return 403 when registration is disabled", async () => {
vi.mocked(getConfig).mockResolvedValue({
auth: { allowRegistration: false, providers: [] },
features: {},
});
const request = createRequest({
name: "Test User",
username: "testuser",
email: "test@example.com",
password: "password123",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe("registration_disabled");
});
});
describe("duplicate checks", () => {
it("should return 400 when email already exists", async () => {
// Mock: email check finds existing user
vi.mocked(db.user.findUnique).mockImplementation(async (args) => {
if (args?.where?.email) {
return { id: "1", email: "test@example.com" } as never;
}
return null;
});
const request = createRequest({
name: "Test User",
username: "testuser",
email: "test@example.com",
password: "password123",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("email_taken");
});
it("should return 400 when username already exists", async () => {
// Mock: email check passes, username check finds existing user
vi.mocked(db.user.findUnique).mockImplementation(async (args) => {
if (args?.where?.email) {
return null;
}
if (args?.where?.username) {
return { id: "1", username: "testuser" } as never;
}
return null;
});
const request = createRequest({
name: "Test User",
username: "testuser",
email: "test@example.com",
password: "password123",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("username_taken");
});
});
describe("successful registration", () => {
it("should create user and return user data", async () => {
vi.mocked(db.user.findUnique).mockResolvedValue(null);
vi.mocked(db.user.create).mockResolvedValue({
id: "user-123",
name: "Test User",
username: "testuser",
email: "test@example.com",
password: "hashed_password",
emailVerified: null,
image: null,
role: "USER",
bio: null,
credits: 0,
createdAt: new Date(),
updatedAt: new Date(),
});
const request = createRequest({
name: "Test User",
username: "testuser",
email: "test@example.com",
password: "password123",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.id).toBe("user-123");
expect(data.name).toBe("Test User");
expect(data.username).toBe("testuser");
expect(data.email).toBe("test@example.com");
expect(data.password).toBeUndefined(); // Password should not be returned
});
it("should accept valid username with underscores", async () => {
vi.mocked(db.user.findUnique).mockResolvedValue(null);
vi.mocked(db.user.create).mockResolvedValue({
id: "user-123",
name: "Test User",
username: "test_user_123",
email: "test@example.com",
password: "hashed_password",
emailVerified: null,
image: null,
role: "USER",
bio: null,
credits: 0,
createdAt: new Date(),
updatedAt: new Date(),
});
const request = createRequest({
name: "Test User",
username: "test_user_123",
email: "test@example.com",
password: "password123",
});
const response = await POST(request);
expect(response.status).toBe(200);
});
});
describe("error handling", () => {
it("should return 500 on database error", async () => {
vi.mocked(db.user.findUnique).mockRejectedValue(new Error("DB Error"));
const request = createRequest({
name: "Test User",
username: "testuser",
email: "test@example.com",
password: "password123",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(500);
expect(data.error).toBe("server_error");
});
it("should return 500 on invalid JSON body", async () => {
const request = new Request("http://localhost:3000/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "invalid json",
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(500);
expect(data.error).toBe("server_error");
});
});
});

View File

@@ -0,0 +1,146 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { getDateLocale, formatDistanceToNow, formatDate } from "@/lib/date";
import { enUS, tr, es, zhCN, ja, arSA } from "date-fns/locale";
describe("getDateLocale", () => {
it("should return enUS for 'en' locale", () => {
expect(getDateLocale("en")).toBe(enUS);
});
it("should return tr for 'tr' locale", () => {
expect(getDateLocale("tr")).toBe(tr);
});
it("should return es for 'es' locale", () => {
expect(getDateLocale("es")).toBe(es);
});
it("should return zhCN for 'zh' locale", () => {
expect(getDateLocale("zh")).toBe(zhCN);
});
it("should return ja for 'ja' locale", () => {
expect(getDateLocale("ja")).toBe(ja);
});
it("should return arSA for 'ar' locale", () => {
expect(getDateLocale("ar")).toBe(arSA);
});
it("should return enUS for unknown locale", () => {
expect(getDateLocale("unknown")).toBe(enUS);
expect(getDateLocale("fr")).toBe(enUS);
expect(getDateLocale("de")).toBe(enUS);
});
});
describe("formatDistanceToNow", () => {
beforeEach(() => {
// Mock the current date to ensure consistent test results
vi.useFakeTimers();
vi.setSystemTime(new Date("2024-01-15T12:00:00Z"));
});
afterEach(() => {
vi.useRealTimers();
});
it("should format a date object relative to now", () => {
const pastDate = new Date("2024-01-14T12:00:00Z");
const result = formatDistanceToNow(pastDate);
expect(result).toContain("day");
expect(result).toContain("ago");
});
it("should format a date string relative to now", () => {
const pastDateString = "2024-01-14T12:00:00Z";
const result = formatDistanceToNow(pastDateString);
expect(result).toContain("day");
expect(result).toContain("ago");
});
it("should format with different locales", () => {
const pastDate = new Date("2024-01-14T12:00:00Z");
// English
const enResult = formatDistanceToNow(pastDate, "en");
expect(enResult).toContain("ago");
// Spanish
const esResult = formatDistanceToNow(pastDate, "es");
expect(esResult).toContain("hace");
// Turkish
const trResult = formatDistanceToNow(pastDate, "tr");
expect(trResult).toContain("önce");
});
it("should handle dates from a week ago", () => {
const weekAgo = new Date("2024-01-08T12:00:00Z");
const result = formatDistanceToNow(weekAgo);
// date-fns may return "7 days ago" instead of "1 week ago"
expect(result).toMatch(/days?|week/);
expect(result).toContain("ago");
});
it("should handle dates from hours ago", () => {
const hoursAgo = new Date("2024-01-15T10:00:00Z");
const result = formatDistanceToNow(hoursAgo);
expect(result).toContain("hours");
expect(result).toContain("ago");
});
it("should default to en locale when none provided", () => {
const pastDate = new Date("2024-01-14T12:00:00Z");
const result = formatDistanceToNow(pastDate);
expect(result).toContain("ago"); // English suffix
});
});
describe("formatDate", () => {
it("should format a date object with the given format string", () => {
const date = new Date("2024-01-15T12:30:45Z");
expect(formatDate(date, "yyyy-MM-dd")).toBe("2024-01-15");
});
it("should format a date string with the given format string", () => {
const dateString = "2024-01-15T12:30:45Z";
expect(formatDate(dateString, "yyyy-MM-dd")).toBe("2024-01-15");
});
it("should format with various format strings", () => {
const date = new Date("2024-06-15T14:30:00Z");
expect(formatDate(date, "MM/dd/yyyy")).toBe("06/15/2024");
expect(formatDate(date, "dd.MM.yyyy")).toBe("15.06.2024");
expect(formatDate(date, "MMMM d, yyyy", "en")).toBe("June 15, 2024");
});
it("should format with different locales", () => {
const date = new Date("2024-06-15T14:30:00Z");
// English month name
const enResult = formatDate(date, "MMMM", "en");
expect(enResult).toBe("June");
// Spanish month name
const esResult = formatDate(date, "MMMM", "es");
expect(esResult).toBe("junio");
// Turkish month name
const trResult = formatDate(date, "MMMM", "tr");
expect(trResult).toBe("Haziran");
});
it("should handle time formatting", () => {
const date = new Date("2024-01-15T14:30:45Z");
expect(formatDate(date, "HH:mm:ss")).toBe("14:30:45");
expect(formatDate(date, "h:mm a", "en")).toMatch(/2:30 PM/i);
});
it("should default to en locale when none provided", () => {
const date = new Date("2024-01-15T12:00:00Z");
const result = formatDate(date, "EEEE"); // Day of week
expect(result).toBe("Monday");
});
});

View File

@@ -0,0 +1,162 @@
import { describe, it, expect } from "vitest";
import { prettifyJson, isValidJson } from "@/lib/format";
describe("prettifyJson", () => {
it("should prettify valid JSON with proper indentation", () => {
const input = '{"name":"John","age":30}';
const expected = `{
"name": "John",
"age": 30
}`;
expect(prettifyJson(input)).toBe(expected);
});
it("should prettify nested JSON objects", () => {
const input = '{"user":{"name":"John","address":{"city":"NYC"}}}';
const result = prettifyJson(input);
expect(result).toContain('"user"');
expect(result).toContain('"address"');
expect(result).toContain('"city"');
expect(result.split("\n").length).toBeGreaterThan(1);
});
it("should prettify JSON arrays", () => {
const input = '[1,2,3,4,5]';
const expected = `[
1,
2,
3,
4,
5
]`;
expect(prettifyJson(input)).toBe(expected);
});
it("should prettify mixed arrays and objects", () => {
const input = '{"items":[{"id":1},{"id":2}]}';
const result = prettifyJson(input);
expect(result).toContain('"items"');
expect(result).toContain('"id"');
expect(result.split("\n").length).toBeGreaterThan(3);
});
it("should return original content for invalid JSON", () => {
const invalidJson = "not valid json";
expect(prettifyJson(invalidJson)).toBe(invalidJson);
});
it("should return original content for malformed JSON", () => {
const malformed = '{"name": "John",}';
expect(prettifyJson(malformed)).toBe(malformed);
});
it("should handle empty object", () => {
expect(prettifyJson("{}")).toBe("{}");
});
it("should handle empty array", () => {
expect(prettifyJson("[]")).toBe("[]");
});
it("should handle JSON with special characters", () => {
const input = '{"message":"Hello\\nWorld"}';
const result = prettifyJson(input);
expect(result).toContain('"message"');
expect(result).toContain("Hello\\nWorld");
});
it("should handle JSON with unicode characters", () => {
const input = '{"emoji":"\\u2764","text":"Hello"}';
const result = prettifyJson(input);
expect(result).toContain('"emoji"');
});
it("should handle boolean and null values", () => {
const input = '{"active":true,"deleted":false,"data":null}';
const result = prettifyJson(input);
expect(result).toContain("true");
expect(result).toContain("false");
expect(result).toContain("null");
});
it("should handle numeric values", () => {
const input = '{"int":42,"float":3.14,"negative":-10}';
const result = prettifyJson(input);
expect(result).toContain("42");
expect(result).toContain("3.14");
expect(result).toContain("-10");
});
});
describe("isValidJson", () => {
it("should return true for valid JSON object", () => {
expect(isValidJson('{"name":"John"}')).toBe(true);
});
it("should return true for valid JSON array", () => {
expect(isValidJson("[1,2,3]")).toBe(true);
});
it("should return true for empty object", () => {
expect(isValidJson("{}")).toBe(true);
});
it("should return true for empty array", () => {
expect(isValidJson("[]")).toBe(true);
});
it("should return true for JSON string primitive", () => {
expect(isValidJson('"hello"')).toBe(true);
});
it("should return true for JSON number primitive", () => {
expect(isValidJson("42")).toBe(true);
expect(isValidJson("3.14")).toBe(true);
});
it("should return true for JSON boolean", () => {
expect(isValidJson("true")).toBe(true);
expect(isValidJson("false")).toBe(true);
});
it("should return true for JSON null", () => {
expect(isValidJson("null")).toBe(true);
});
it("should return false for invalid JSON", () => {
expect(isValidJson("not json")).toBe(false);
});
it("should return false for malformed JSON with trailing comma", () => {
expect(isValidJson('{"name": "John",}')).toBe(false);
});
it("should return false for single quotes (non-standard)", () => {
expect(isValidJson("{'name': 'John'}")).toBe(false);
});
it("should return false for unquoted keys", () => {
expect(isValidJson("{name: 'John'}")).toBe(false);
});
it("should return false for empty string", () => {
expect(isValidJson("")).toBe(false);
});
it("should return false for undefined-like string", () => {
expect(isValidJson("undefined")).toBe(false);
});
it("should return true for nested valid JSON", () => {
const nested = '{"level1":{"level2":{"level3":"value"}}}';
expect(isValidJson(nested)).toBe(true);
});
it("should return true for JSON with whitespace", () => {
const withWhitespace = `{
"name": "John",
"age": 30
}`;
expect(isValidJson(withWhitespace)).toBe(true);
});
});

View File

@@ -0,0 +1,115 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { cn, isChromeBrowser } from "@/lib/utils";
describe("cn (className utility)", () => {
it("should merge class names", () => {
expect(cn("foo", "bar")).toBe("foo bar");
});
it("should handle conditional classes", () => {
expect(cn("foo", false && "bar", "baz")).toBe("foo baz");
expect(cn("foo", true && "bar", "baz")).toBe("foo bar baz");
});
it("should merge tailwind classes correctly", () => {
expect(cn("px-2 py-1", "px-4")).toBe("py-1 px-4");
expect(cn("text-red-500", "text-blue-500")).toBe("text-blue-500");
});
it("should handle arrays of class names", () => {
expect(cn(["foo", "bar"], "baz")).toBe("foo bar baz");
});
it("should handle objects with boolean values", () => {
expect(cn({ foo: true, bar: false, baz: true })).toBe("foo baz");
});
it("should handle undefined and null values", () => {
expect(cn("foo", undefined, null, "bar")).toBe("foo bar");
});
it("should handle empty strings", () => {
expect(cn("foo", "", "bar")).toBe("foo bar");
});
it("should return empty string for no arguments", () => {
expect(cn()).toBe("");
});
it("should handle complex tailwind class conflicts", () => {
// Background color conflict
expect(cn("bg-red-500", "bg-blue-500")).toBe("bg-blue-500");
// Margin conflict
expect(cn("m-2", "m-4")).toBe("m-4");
// Padding with direction conflict
expect(cn("p-2", "px-4")).toBe("p-2 px-4");
});
});
describe("isChromeBrowser", () => {
const originalWindow = global.window;
const originalNavigator = global.navigator;
beforeEach(() => {
// Reset window and navigator before each test
vi.stubGlobal("window", { ...originalWindow });
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("should return false when window is undefined (SSR)", () => {
vi.stubGlobal("window", undefined);
expect(isChromeBrowser()).toBe(false);
});
it("should return true for Chrome browser", () => {
vi.stubGlobal("navigator", {
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
});
expect(isChromeBrowser()).toBe(true);
});
it("should return true for Edge browser", () => {
vi.stubGlobal("navigator", {
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
});
expect(isChromeBrowser()).toBe(true);
});
it("should return true for Opera browser", () => {
vi.stubGlobal("navigator", {
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0",
});
expect(isChromeBrowser()).toBe(true);
});
it("should return true for Brave browser", () => {
vi.stubGlobal("navigator", {
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
brave: { isBrave: () => Promise.resolve(true) },
});
expect(isChromeBrowser()).toBe(true);
});
it("should return false for Firefox browser", () => {
vi.stubGlobal("navigator", {
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
});
expect(isChromeBrowser()).toBe(false);
});
it("should return false for Safari browser", () => {
vi.stubGlobal("navigator", {
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
});
expect(isChromeBrowser()).toBe(false);
});
});

View File

@@ -0,0 +1,352 @@
import { describe, it, expect } from "vitest";
import {
detectVariables,
convertToSupportedFormat,
convertAllVariables,
getPatternDescription,
type DetectedVariable,
} from "@/lib/variable-detection";
describe("detectVariables", () => {
describe("double bracket pattern [[...]]", () => {
it("should detect [[name]] format", () => {
const result = detectVariables("Hello [[name]]!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("name");
expect(result[0].pattern).toBe("double_bracket");
expect(result[0].original).toBe("[[name]]");
});
it("should detect [[name]] with spaces", () => {
const result = detectVariables("Hello [[ name ]]!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("name");
});
it("should detect [[name: default]] with default value", () => {
const result = detectVariables("Hello [[name: John]]!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("name");
expect(result[0].defaultValue).toBe("John");
});
it("should detect multiple [[...]] variables", () => {
const result = detectVariables("Hello [[first_name]] [[last_name]]!");
expect(result).toHaveLength(2);
expect(result[0].name).toBe("first_name");
expect(result[1].name).toBe("last_name");
});
});
describe("double curly pattern {{...}}", () => {
it("should detect {{name}} format", () => {
const result = detectVariables("Hello {{name}}!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("name");
expect(result[0].pattern).toBe("double_curly");
});
it("should detect {{name}} with spaces", () => {
const result = detectVariables("Hello {{ name }}!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("name");
});
it("should detect {{name: default}} with default value", () => {
const result = detectVariables("Hello {{name: Jane}}!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("name");
expect(result[0].defaultValue).toBe("Jane");
});
});
describe("single bracket pattern [...]", () => {
it("should detect [NAME] uppercase format", () => {
const result = detectVariables("Hello [NAME]!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("NAME");
expect(result[0].pattern).toBe("single_bracket");
});
it("should detect [Your Name] multi-word format", () => {
const result = detectVariables("Hello [Your Name]!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("Your Name");
});
it("should detect [USER_ID] with underscores", () => {
const result = detectVariables("ID: [USER_ID]");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("USER_ID");
});
});
describe("single curly pattern {...}", () => {
it("should detect {NAME} uppercase format", () => {
const result = detectVariables("Hello {NAME}!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("NAME");
expect(result[0].pattern).toBe("single_curly");
});
it("should detect {USER_ID} with underscores", () => {
const result = detectVariables("ID: {USER_ID}");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("USER_ID");
});
});
describe("angle bracket pattern <...>", () => {
it("should detect <NAME> uppercase format", () => {
const result = detectVariables("Hello <NAME>!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("NAME");
expect(result[0].pattern).toBe("angle_bracket");
});
it("should detect <Your Name> with spaces", () => {
const result = detectVariables("Hello <Your Name>!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("Your Name");
});
it("should NOT detect common HTML tags", () => {
const result = detectVariables("<div>content</div>");
expect(result).toHaveLength(0);
});
it("should NOT detect lowercase single words (looks like HTML)", () => {
const result = detectVariables("<username>");
expect(result).toHaveLength(0);
});
});
describe("percent pattern %...%", () => {
it("should detect %NAME% format", () => {
const result = detectVariables("Hello %NAME%!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("NAME");
expect(result[0].pattern).toBe("percent");
});
it("should detect %user_name% lowercase with underscore", () => {
const result = detectVariables("Hello %user_name%!");
expect(result).toHaveLength(1);
expect(result[0].name).toBe("user_name");
});
});
describe("dollar curly pattern ${...} (supported format)", () => {
it("should NOT detect ${name} as it is the supported format", () => {
const result = detectVariables("Hello ${name}!");
expect(result).toHaveLength(0);
});
it("should NOT detect ${name:default} with default", () => {
const result = detectVariables("Hello ${name:John}!");
expect(result).toHaveLength(0);
});
});
describe("false positive handling", () => {
it("should skip HTML tags", () => {
const htmlTags = [
"<div>",
"<span>",
"<p>",
"<a>",
"<button>",
"<input>",
"<form>",
"<table>",
];
for (const tag of htmlTags) {
const result = detectVariables(tag);
expect(result).toHaveLength(0);
}
});
it("should skip programming keywords", () => {
// These would be in angle brackets but should be filtered
const result = detectVariables("<IF> <ELSE> <FOR>");
expect(result).toHaveLength(0);
});
it("should skip very short names", () => {
const result = detectVariables("[[a]] {{b}}");
expect(result).toHaveLength(0);
});
});
describe("mixed patterns", () => {
it("should detect multiple different patterns", () => {
const text = "Hello [[name]], your ID is {{user_id}} and role is [ROLE]";
const result = detectVariables(text);
expect(result).toHaveLength(3);
expect(result.map((v) => v.pattern)).toContain("double_bracket");
expect(result.map((v) => v.pattern)).toContain("double_curly");
expect(result.map((v) => v.pattern)).toContain("single_bracket");
});
it("should handle text with supported format mixed in", () => {
const text = "Hello ${name}, welcome [[user]]!";
const result = detectVariables(text);
expect(result).toHaveLength(1);
expect(result[0].name).toBe("user");
});
});
describe("position tracking", () => {
it("should track start and end indices correctly", () => {
const text = "Hello [[name]]!";
const result = detectVariables(text);
expect(result[0].startIndex).toBe(6);
expect(result[0].endIndex).toBe(14);
expect(text.slice(result[0].startIndex, result[0].endIndex)).toBe(
"[[name]]"
);
});
it("should sort results by position", () => {
const text = "{{second}} first [[first]] text";
const result = detectVariables(text);
expect(result[0].startIndex).toBeLessThan(result[1].startIndex);
});
});
});
describe("convertToSupportedFormat", () => {
it("should convert variable name to lowercase", () => {
const variable: DetectedVariable = {
original: "[[NAME]]",
name: "NAME",
pattern: "double_bracket",
startIndex: 0,
endIndex: 8,
};
expect(convertToSupportedFormat(variable)).toBe("${name}");
});
it("should replace spaces with underscores", () => {
const variable: DetectedVariable = {
original: "[[Your Name]]",
name: "Your Name",
pattern: "double_bracket",
startIndex: 0,
endIndex: 13,
};
expect(convertToSupportedFormat(variable)).toBe("${your_name}");
});
it("should include default value if present", () => {
const variable: DetectedVariable = {
original: "[[name: John]]",
name: "name",
defaultValue: "John",
pattern: "double_bracket",
startIndex: 0,
endIndex: 14,
};
expect(convertToSupportedFormat(variable)).toBe("${name:John}");
});
it("should handle multiple spaces", () => {
const variable: DetectedVariable = {
original: "[[First Middle Last]]",
name: "First Middle Last",
pattern: "double_bracket",
startIndex: 0,
endIndex: 21,
};
expect(convertToSupportedFormat(variable)).toBe("${first_middle_last}");
});
it("should remove special characters", () => {
const variable: DetectedVariable = {
original: "[[user@name]]",
name: "user@name",
pattern: "double_bracket",
startIndex: 0,
endIndex: 13,
};
expect(convertToSupportedFormat(variable)).toBe("${username}");
});
});
describe("convertAllVariables", () => {
it("should convert all detected variables in text", () => {
const text = "Hello [[name]], your ID is {{user_id}}";
const result = convertAllVariables(text);
expect(result).toBe("Hello ${name}, your ID is ${user_id}");
});
it("should preserve text that has no variables", () => {
const text = "Hello world, no variables here!";
expect(convertAllVariables(text)).toBe(text);
});
it("should handle multiple variables of same pattern", () => {
const text = "[[first]] and [[second]] and [[third]]";
const result = convertAllVariables(text);
expect(result).toBe("${first} and ${second} and ${third}");
});
it("should preserve already supported format", () => {
const text = "Hello ${name}!";
expect(convertAllVariables(text)).toBe(text);
});
it("should handle mixed supported and unsupported formats", () => {
const text = "Hello ${name}, welcome [[user]]!";
const result = convertAllVariables(text);
expect(result).toBe("Hello ${name}, welcome ${user}!");
});
it("should convert with default values", () => {
const text = "Hello [[name: World]]!";
const result = convertAllVariables(text);
expect(result).toBe("Hello ${name:World}!");
});
it("should handle variables at the start and end", () => {
const text = "[[start]] middle [[end]]";
const result = convertAllVariables(text);
expect(result).toBe("${start} middle ${end}");
});
it("should handle adjacent variables", () => {
const text = "[[first]][[second]]";
const result = convertAllVariables(text);
expect(result).toBe("${first}${second}");
});
});
describe("getPatternDescription", () => {
it("should return correct description for double_bracket", () => {
expect(getPatternDescription("double_bracket")).toBe("[[...]]");
});
it("should return correct description for double_curly", () => {
expect(getPatternDescription("double_curly")).toBe("{{...}}");
});
it("should return correct description for single_bracket", () => {
expect(getPatternDescription("single_bracket")).toBe("[...]");
});
it("should return correct description for single_curly", () => {
expect(getPatternDescription("single_curly")).toBe("{...}");
});
it("should return correct description for angle_bracket", () => {
expect(getPatternDescription("angle_bracket")).toBe("<...>");
});
it("should return correct description for percent", () => {
expect(getPatternDescription("percent")).toBe("%...%");
});
it("should return correct description for dollar_curly", () => {
expect(getPatternDescription("dollar_curly")).toBe("${...}");
});
});

31
vitest.config.ts Normal file
View File

@@ -0,0 +1,31 @@
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./vitest.setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
exclude: ["node_modules", ".next", "packages"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: [
"node_modules/",
".next/",
"packages/",
"src/**/*.d.ts",
"vitest.config.ts",
"vitest.setup.ts",
],
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});

89
vitest.setup.ts Normal file
View File

@@ -0,0 +1,89 @@
import "@testing-library/jest-dom";
import { vi } from "vitest";
// Mock environment variables
process.env.NEXTAUTH_SECRET = "test-secret";
process.env.NEXTAUTH_URL = "http://localhost:3000";
process.env.DATABASE_URL = "postgresql://test:test@localhost:5432/test";
// Mock next/navigation
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
refresh: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
prefetch: vi.fn(),
}),
useSearchParams: () => new URLSearchParams(),
usePathname: () => "/",
useParams: () => ({}),
redirect: vi.fn(),
notFound: vi.fn(),
}));
// Mock next/headers
vi.mock("next/headers", () => ({
cookies: () => ({
get: vi.fn(),
set: vi.fn(),
delete: vi.fn(),
}),
headers: () => new Headers(),
}));
// Mock next-intl
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
getTranslations: () => Promise.resolve((key: string) => key),
}));
// Mock Prisma client
vi.mock("@/lib/db", () => ({
db: {
user: {
findUnique: vi.fn(),
findFirst: vi.fn(),
findMany: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
count: vi.fn(),
},
prompt: {
findUnique: vi.fn(),
findFirst: vi.fn(),
findMany: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
count: vi.fn(),
},
category: {
findUnique: vi.fn(),
findFirst: vi.fn(),
findMany: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
},
$queryRaw: vi.fn(),
$executeRaw: vi.fn(),
$transaction: vi.fn(),
},
}));
// Mock config
vi.mock("@/lib/config", () => ({
getConfig: vi.fn(() =>
Promise.resolve({
auth: {
allowRegistration: true,
providers: [],
},
features: {},
})
),
}));