Initial commit
This commit is contained in:
59
__tests__/components/LoadingSpinner.test.tsx
Normal file
59
__tests__/components/LoadingSpinner.test.tsx
Normal 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");
|
||||
});
|
||||
});
|
||||
123
__tests__/errors/app-error.test.ts
Normal file
123
__tests__/errors/app-error.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
62
__tests__/features/theme-context.test.tsx
Normal file
62
__tests__/features/theme-context.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
64
__tests__/features/user-settings.test.ts
Normal file
64
__tests__/features/user-settings.test.ts
Normal 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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
44
__tests__/utils/cn.test.ts
Normal file
44
__tests__/utils/cn.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user