Initial commit

This commit is contained in:
theshy
2025-07-31 17:05:07 +08:00
parent 8fab3b19cc
commit 24f21144ab
91 changed files with 16311 additions and 159 deletions

View File

@ -0,0 +1,59 @@
import { render, screen } from "@testing-library/react";
import LoadingSpinner from "@/components/LoadingSpinner";
describe("LoadingSpinner Component", () => {
it("should render with default props", () => {
render(<LoadingSpinner />);
// 检查是否有 SVG 元素存在
const svg = document.querySelector("svg");
expect(svg).toBeTruthy();
});
it("should render with custom text", () => {
render(<LoadingSpinner text="Loading..." />);
const text = screen.getByText("Loading...");
expect(text).toBeTruthy();
});
it("should render with different sizes", () => {
const { rerender } = render(<LoadingSpinner size="sm" />);
let spinner = document.querySelector(".w-4.h-4");
expect(spinner).toBeTruthy();
rerender(<LoadingSpinner size="lg" />);
spinner = document.querySelector(".w-12.h-12");
expect(spinner).toBeTruthy();
});
it("should render with different colors", () => {
const { rerender } = render(<LoadingSpinner color="blue" />);
let spinner = document.querySelector(".text-blue-500");
expect(spinner).toBeTruthy();
rerender(<LoadingSpinner color="red" />);
spinner = document.querySelector(".text-red-500");
expect(spinner).toBeTruthy();
});
it("should apply correct size classes", () => {
const { rerender } = render(<LoadingSpinner size="sm" />);
let spinner = document.querySelector(".w-4.h-4");
expect(spinner?.className).toContain("w-4");
expect(spinner?.className).toContain("h-4");
rerender(<LoadingSpinner size="lg" />);
spinner = document.querySelector(".w-12.h-12");
expect(spinner?.className).toContain("w-12");
expect(spinner?.className).toContain("h-12");
});
it("should apply correct color classes", () => {
const { rerender } = render(<LoadingSpinner color="blue" />);
let spinner = document.querySelector(".text-blue-500");
expect(spinner?.className).toContain("text-blue-500");
rerender(<LoadingSpinner color="red" />);
spinner = document.querySelector(".text-red-500");
expect(spinner?.className).toContain("text-red-500");
});
});

View File

@ -0,0 +1,123 @@
import {
AppError,
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
ConflictError,
RateLimitError,
InternalServerError,
} from "@/lib/errors/app-error";
describe("AppError classes", () => {
describe("AppError", () => {
it("should create an AppError with default values", () => {
const error = new AppError("Test error");
expect(error.message).toBe("Test error");
expect(error.statusCode).toBe(500);
expect(error.isOperational).toBe(true);
expect(error.code).toBeUndefined();
});
it("should create an AppError with custom values", () => {
const error = new AppError("Custom error", 400, "CUSTOM_ERROR", false);
expect(error.message).toBe("Custom error");
expect(error.statusCode).toBe(400);
expect(error.isOperational).toBe(false);
expect(error.code).toBe("CUSTOM_ERROR");
});
});
describe("ValidationError", () => {
it("should create a ValidationError with default code", () => {
const error = new ValidationError("Invalid input");
expect(error.message).toBe("Invalid input");
expect(error.statusCode).toBe(400);
expect(error.code).toBe("VALIDATION_ERROR");
});
it("should create a ValidationError with custom code", () => {
const error = new ValidationError(
"Invalid input",
"CUSTOM_VALIDATION_ERROR"
);
expect(error.message).toBe("Invalid input");
expect(error.statusCode).toBe(400);
expect(error.code).toBe("CUSTOM_VALIDATION_ERROR");
});
});
describe("AuthenticationError", () => {
it("should create an AuthenticationError with default message", () => {
const error = new AuthenticationError();
expect(error.message).toBe("未授权访问");
expect(error.statusCode).toBe(401);
expect(error.code).toBe("AUTHENTICATION_ERROR");
});
it("should create an AuthenticationError with custom message", () => {
const error = new AuthenticationError("Custom auth error");
expect(error.message).toBe("Custom auth error");
expect(error.statusCode).toBe(401);
expect(error.code).toBe("AUTHENTICATION_ERROR");
});
});
describe("AuthorizationError", () => {
it("should create an AuthorizationError with default message", () => {
const error = new AuthorizationError();
expect(error.message).toBe("权限不足");
expect(error.statusCode).toBe(403);
expect(error.code).toBe("AUTHORIZATION_ERROR");
});
});
describe("NotFoundError", () => {
it("should create a NotFoundError with default message", () => {
const error = new NotFoundError();
expect(error.message).toBe("资源不存在");
expect(error.statusCode).toBe(404);
expect(error.code).toBe("NOT_FOUND_ERROR");
});
});
describe("ConflictError", () => {
it("should create a ConflictError with default code", () => {
const error = new ConflictError("Resource conflict");
expect(error.message).toBe("Resource conflict");
expect(error.statusCode).toBe(409);
expect(error.code).toBe("CONFLICT_ERROR");
});
});
describe("RateLimitError", () => {
it("should create a RateLimitError with default message", () => {
const error = new RateLimitError();
expect(error.message).toBe("请求过于频繁");
expect(error.statusCode).toBe(429);
expect(error.code).toBe("RATE_LIMIT_ERROR");
});
});
describe("InternalServerError", () => {
it("should create an InternalServerError with default message", () => {
const error = new InternalServerError();
expect(error.message).toBe("服务器内部错误");
expect(error.statusCode).toBe(500);
expect(error.code).toBe("INTERNAL_SERVER_ERROR");
expect(error.isOperational).toBe(false);
});
});
describe("Error inheritance", () => {
it("should be instanceof Error", () => {
const error = new AppError("Test");
expect(error).toBeInstanceOf(Error);
});
it("should be instanceof AppError", () => {
const error = new ValidationError("Test");
expect(error).toBeInstanceOf(AppError);
});
});
});

View File

@ -0,0 +1,62 @@
import { render, screen } from "@testing-library/react";
import { ThemeProvider, useTheme } from "@/lib/contexts/theme-context";
// 测试组件
function TestComponent() {
const { theme, setTheme, isDark } = useTheme();
return (
<div>
<span data-testid="theme">{theme}</span>
<span data-testid="isDark">{isDark.toString()}</span>
<button onClick={() => setTheme("dark")}>Set Dark</button>
<button onClick={() => setTheme("light")}>Set Light</button>
</div>
);
}
describe("ThemeContext", () => {
beforeEach(() => {
// 清除 localStorage
localStorage.clear();
// 清除 document 的 class
document.documentElement.classList.remove("dark");
document.body.classList.remove("dark");
});
it("should provide default theme", () => {
render(
<ThemeProvider>
<TestComponent />
</ThemeProvider>
);
expect(screen.getByTestId("theme").textContent).toBe("light");
expect(screen.getByTestId("isDark").textContent).toBe("false");
});
it("should load theme from localStorage", () => {
localStorage.setItem("theme", "dark");
render(
<ThemeProvider>
<TestComponent />
</ThemeProvider>
);
expect(screen.getByTestId("theme").textContent).toBe("dark");
expect(screen.getByTestId("isDark").textContent).toBe("true");
});
it("should apply dark class to document", () => {
localStorage.setItem("theme", "dark");
render(
<ThemeProvider>
<TestComponent />
</ThemeProvider>
);
expect(document.documentElement.classList.contains("dark")).toBe(true);
expect(document.body.classList.contains("dark")).toBe(true);
});
});

View File

@ -0,0 +1,64 @@
import { UserSettingsService } from "@/lib/services/user-settings.service";
// Mock Prisma
jest.mock("@/lib/database", () => ({
prisma: {
userSettings: {
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
upsert: jest.fn(),
},
},
}));
describe("UserSettingsService", () => {
const mockPrisma = require("@/lib/database").prisma;
beforeEach(() => {
jest.clearAllMocks();
});
it("should get user settings", async () => {
const mockSettings = {
id: "settings-1",
userId: "user-1",
defaultQuality: "medium",
publicProfile: false,
allowDownload: true,
createdAt: new Date(),
updatedAt: new Date(),
};
mockPrisma.userSettings.findUnique.mockResolvedValue(mockSettings);
const result = await UserSettingsService.getUserSettings("user-1");
expect(result).toEqual(mockSettings);
expect(mockPrisma.userSettings.findUnique).toHaveBeenCalledWith({
where: { userId: "user-1" },
});
});
it("should return existing user settings when they exist", async () => {
const mockSettings = {
id: "settings-1",
userId: "user-1",
defaultQuality: "medium",
publicProfile: false,
allowDownload: true,
createdAt: new Date(),
updatedAt: new Date(),
};
mockPrisma.userSettings.findUnique.mockResolvedValue(mockSettings);
const result = await UserSettingsService.getOrCreateUserSettings("user-1");
expect(result).toEqual(mockSettings);
expect(mockPrisma.userSettings.findUnique).toHaveBeenCalledWith({
where: { userId: "user-1" },
});
});
});

View File

@ -0,0 +1,44 @@
import { cn } from '@/lib/utils/cn'
describe('cn utility function', () => {
it('should merge class names correctly', () => {
const result = cn('text-red-500', 'bg-blue-500', 'p-4')
expect(result).toBe('text-red-500 bg-blue-500 p-4')
})
it('should handle conditional classes', () => {
const isActive = true
const result = cn('base-class', isActive && 'active-class', 'always-class')
expect(result).toBe('base-class active-class always-class')
})
it('should handle false conditional classes', () => {
const isActive = false
const result = cn('base-class', isActive && 'active-class', 'always-class')
expect(result).toBe('base-class always-class')
})
it('should handle arrays of classes', () => {
const result = cn(['class1', 'class2'], 'class3')
expect(result).toBe('class1 class2 class3')
})
it('should handle objects with boolean values', () => {
const result = cn({
'class1': true,
'class2': false,
'class3': true
})
expect(result).toBe('class1 class3')
})
it('should handle empty inputs', () => {
const result = cn()
expect(result).toBe('')
})
it('should handle mixed inputs', () => {
const result = cn('base', ['array1', 'array2'], { 'obj1': true, 'obj2': false }, 'string')
expect(result).toBe('base array1 array2 obj1 string')
})
})