Spaces:
Sleeping
Sleeping
Commit
·
a7f988b
1
Parent(s):
3d19d88
test: 유틸리티 함수 테스트 추가 및 수정
Browse files- jest.config.js +26 -0
- package-lock.json +0 -0
- package.json +9 -1
- src/lib/__tests__/utils.test.ts +127 -0
- src/lib/utils.ts +34 -36
- src/setupTests.ts +19 -0
- src/types/incentive.ts +3 -3
jest.config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
preset: 'ts-jest',
|
| 3 |
+
testEnvironment: 'node',
|
| 4 |
+
roots: ['<rootDir>/src'],
|
| 5 |
+
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
|
| 6 |
+
transform: {
|
| 7 |
+
'^.+\\.ts$': 'ts-jest',
|
| 8 |
+
},
|
| 9 |
+
moduleNameMapper: {
|
| 10 |
+
'^@/(.*)$': '<rootDir>/src/$1',
|
| 11 |
+
},
|
| 12 |
+
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
|
| 13 |
+
collectCoverageFrom: [
|
| 14 |
+
'src/**/*.{ts,tsx}',
|
| 15 |
+
'!src/**/*.d.ts',
|
| 16 |
+
'!src/**/__tests__/**',
|
| 17 |
+
],
|
| 18 |
+
coverageThreshold: {
|
| 19 |
+
global: {
|
| 20 |
+
branches: 80,
|
| 21 |
+
functions: 80,
|
| 22 |
+
lines: 80,
|
| 23 |
+
statements: 80,
|
| 24 |
+
},
|
| 25 |
+
},
|
| 26 |
+
};
|
package-lock.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
CHANGED
|
@@ -6,7 +6,10 @@
|
|
| 6 |
"dev": "next dev --turbopack",
|
| 7 |
"build": "next build",
|
| 8 |
"start": "next start",
|
| 9 |
-
"lint": "next lint"
|
|
|
|
|
|
|
|
|
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
"@graphql-tools/load-files": "^7.0.1",
|
|
@@ -35,12 +38,17 @@
|
|
| 35 |
"devDependencies": {
|
| 36 |
"@eslint/eslintrc": "^3",
|
| 37 |
"@tailwindcss/postcss": "^4",
|
|
|
|
|
|
|
|
|
|
| 38 |
"@types/node": "^20",
|
| 39 |
"@types/react": "^19",
|
| 40 |
"@types/react-dom": "^19",
|
| 41 |
"eslint": "^9",
|
| 42 |
"eslint-config-next": "15.2.4",
|
|
|
|
| 43 |
"tailwindcss": "^4",
|
|
|
|
| 44 |
"typescript": "^5"
|
| 45 |
}
|
| 46 |
}
|
|
|
|
| 6 |
"dev": "next dev --turbopack",
|
| 7 |
"build": "next build",
|
| 8 |
"start": "next start",
|
| 9 |
+
"lint": "next lint",
|
| 10 |
+
"test": "jest",
|
| 11 |
+
"test:watch": "jest --watch",
|
| 12 |
+
"test:coverage": "jest --coverage"
|
| 13 |
},
|
| 14 |
"dependencies": {
|
| 15 |
"@graphql-tools/load-files": "^7.0.1",
|
|
|
|
| 38 |
"devDependencies": {
|
| 39 |
"@eslint/eslintrc": "^3",
|
| 40 |
"@tailwindcss/postcss": "^4",
|
| 41 |
+
"@testing-library/jest-dom": "^6.6.3",
|
| 42 |
+
"@testing-library/react": "^16.3.0",
|
| 43 |
+
"@types/jest": "^29.5.14",
|
| 44 |
"@types/node": "^20",
|
| 45 |
"@types/react": "^19",
|
| 46 |
"@types/react-dom": "^19",
|
| 47 |
"eslint": "^9",
|
| 48 |
"eslint-config-next": "15.2.4",
|
| 49 |
+
"jest": "^29.7.0",
|
| 50 |
"tailwindcss": "^4",
|
| 51 |
+
"ts-jest": "^29.3.1",
|
| 52 |
"typescript": "^5"
|
| 53 |
}
|
| 54 |
}
|
src/lib/__tests__/utils.test.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
formatDate,
|
| 3 |
+
formatPoints,
|
| 4 |
+
getContributionTypeLabel,
|
| 5 |
+
getLevelTitle,
|
| 6 |
+
getRarityColor,
|
| 7 |
+
calculateLevelProgress,
|
| 8 |
+
generateRandomString,
|
| 9 |
+
truncateText,
|
| 10 |
+
formatFileSize,
|
| 11 |
+
isValidUrl,
|
| 12 |
+
getInitials,
|
| 13 |
+
groupBy,
|
| 14 |
+
} from '../utils';
|
| 15 |
+
import { ContributionType } from '../../types/incentive';
|
| 16 |
+
|
| 17 |
+
describe('Utils', () => {
|
| 18 |
+
describe('formatDate', () => {
|
| 19 |
+
it('should format date correctly', () => {
|
| 20 |
+
const date = new Date('2024-01-01');
|
| 21 |
+
expect(formatDate(date)).toBe('2024. 1. 1.');
|
| 22 |
+
});
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
describe('formatPoints', () => {
|
| 26 |
+
it('should format points correctly', () => {
|
| 27 |
+
expect(formatPoints(1000)).toBe('1,000');
|
| 28 |
+
expect(formatPoints(1000000)).toBe('1,000,000');
|
| 29 |
+
});
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
describe('getContributionTypeLabel', () => {
|
| 33 |
+
it('should return correct label for each contribution type', () => {
|
| 34 |
+
expect(getContributionTypeLabel(ContributionType.POST)).toBe('게시글 작성');
|
| 35 |
+
expect(getContributionTypeLabel(ContributionType.COMMENT)).toBe('댓글 작성');
|
| 36 |
+
expect(getContributionTypeLabel(ContributionType.REVIEW)).toBe('코드 리뷰');
|
| 37 |
+
});
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
describe('getLevelTitle', () => {
|
| 41 |
+
it('should return correct title for each level', () => {
|
| 42 |
+
expect(getLevelTitle(1)).toBe('초보자');
|
| 43 |
+
expect(getLevelTitle(5)).toBe('중급자');
|
| 44 |
+
expect(getLevelTitle(10)).toBe('고급자');
|
| 45 |
+
expect(getLevelTitle(20)).toBe('마스터');
|
| 46 |
+
expect(getLevelTitle(30)).toBe('레전드');
|
| 47 |
+
});
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
describe('getRarityColor', () => {
|
| 51 |
+
it('should return correct color for each rarity', () => {
|
| 52 |
+
expect(getRarityColor('common')).toBe('#808080');
|
| 53 |
+
expect(getRarityColor('rare')).toBe('#4169E1');
|
| 54 |
+
expect(getRarityColor('epic')).toBe('#9932CC');
|
| 55 |
+
expect(getRarityColor('legendary')).toBe('#FFD700');
|
| 56 |
+
});
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
describe('calculateLevelProgress', () => {
|
| 60 |
+
it('should calculate level progress correctly', () => {
|
| 61 |
+
const result = calculateLevelProgress(1500);
|
| 62 |
+
expect(result.current).toBe(2);
|
| 63 |
+
expect(result.next).toBe(3);
|
| 64 |
+
expect(result.progress).toBeGreaterThanOrEqual(0);
|
| 65 |
+
expect(result.progress).toBeLessThanOrEqual(100);
|
| 66 |
+
});
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
describe('generateRandomString', () => {
|
| 70 |
+
it('should generate string of correct length', () => {
|
| 71 |
+
const length = 10;
|
| 72 |
+
const result = generateRandomString(length);
|
| 73 |
+
expect(result).toHaveLength(length);
|
| 74 |
+
});
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
describe('truncateText', () => {
|
| 78 |
+
it('should truncate text correctly', () => {
|
| 79 |
+
const text = 'This is a long text that needs to be truncated';
|
| 80 |
+
const maxLength = 10;
|
| 81 |
+
const result = truncateText(text, maxLength);
|
| 82 |
+
expect(result).toHaveLength(maxLength + 3); // +3 for '...'
|
| 83 |
+
expect(result.endsWith('...')).toBe(true);
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
it('should not truncate short text', () => {
|
| 87 |
+
const text = 'Short';
|
| 88 |
+
const result = truncateText(text, 10);
|
| 89 |
+
expect(result).toBe(text);
|
| 90 |
+
});
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
describe('formatFileSize', () => {
|
| 94 |
+
it('should format file size correctly', () => {
|
| 95 |
+
expect(formatFileSize(1024)).toBe('1 KB');
|
| 96 |
+
expect(formatFileSize(1024 * 1024)).toBe('1 MB');
|
| 97 |
+
expect(formatFileSize(1024 * 1024 * 1024)).toBe('1 GB');
|
| 98 |
+
});
|
| 99 |
+
});
|
| 100 |
+
|
| 101 |
+
describe('isValidUrl', () => {
|
| 102 |
+
it('should validate URLs correctly', () => {
|
| 103 |
+
expect(isValidUrl('https://example.com')).toBe(true);
|
| 104 |
+
expect(isValidUrl('not-a-url')).toBe(false);
|
| 105 |
+
});
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
describe('getInitials', () => {
|
| 109 |
+
it('should get initials correctly', () => {
|
| 110 |
+
expect(getInitials('John Doe')).toBe('JD');
|
| 111 |
+
expect(getInitials('Alice')).toBe('A');
|
| 112 |
+
});
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
+
describe('groupBy', () => {
|
| 116 |
+
it('should group items correctly', () => {
|
| 117 |
+
const items = [
|
| 118 |
+
{ category: 'A', value: 1 },
|
| 119 |
+
{ category: 'B', value: 2 },
|
| 120 |
+
{ category: 'A', value: 3 },
|
| 121 |
+
];
|
| 122 |
+
const result = groupBy(items, 'category');
|
| 123 |
+
expect(result['A']).toHaveLength(2);
|
| 124 |
+
expect(result['B']).toHaveLength(1);
|
| 125 |
+
});
|
| 126 |
+
});
|
| 127 |
+
});
|
src/lib/utils.ts
CHANGED
|
@@ -1,62 +1,60 @@
|
|
| 1 |
import { ContributionType } from '../types/incentive';
|
| 2 |
|
| 3 |
-
export
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
minute: '2-digit',
|
| 10 |
-
}).format(date);
|
| 11 |
-
}
|
| 12 |
|
| 13 |
-
export
|
| 14 |
-
return
|
| 15 |
-
}
|
| 16 |
|
| 17 |
-
export
|
| 18 |
const labels: Record<ContributionType, string> = {
|
| 19 |
-
[ContributionType.
|
| 20 |
[ContributionType.COMMENT]: '댓글 작성',
|
| 21 |
-
[ContributionType.REVIEW]: '리뷰
|
| 22 |
[ContributionType.BUG_REPORT]: '버그 리포트',
|
| 23 |
-
[ContributionType.
|
| 24 |
-
[ContributionType.
|
| 25 |
};
|
| 26 |
return labels[type];
|
| 27 |
-
}
|
| 28 |
|
| 29 |
-
export
|
| 30 |
-
if (level
|
| 31 |
-
if (level
|
| 32 |
-
if (level
|
| 33 |
-
if (level
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
}
|
| 37 |
|
| 38 |
-
export
|
| 39 |
const colors: Record<string, string> = {
|
| 40 |
common: '#808080',
|
| 41 |
rare: '#4169E1',
|
| 42 |
epic: '#9932CC',
|
| 43 |
legendary: '#FFD700',
|
| 44 |
};
|
| 45 |
-
return colors[rarity] || '#
|
| 46 |
-
}
|
| 47 |
|
| 48 |
-
export
|
| 49 |
-
const
|
| 50 |
-
const
|
| 51 |
-
const
|
|
|
|
|
|
|
| 52 |
const progress = ((points - currentLevelPoints) / (nextLevelPoints - currentLevelPoints)) * 100;
|
| 53 |
|
| 54 |
return {
|
| 55 |
current: currentLevel,
|
| 56 |
-
next:
|
| 57 |
-
progress,
|
| 58 |
};
|
| 59 |
-
}
|
| 60 |
|
| 61 |
export function generateRandomString(length: number): string {
|
| 62 |
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
|
|
| 1 |
import { ContributionType } from '../types/incentive';
|
| 2 |
|
| 3 |
+
export const formatDate = (date: Date): string => {
|
| 4 |
+
const year = date.getFullYear();
|
| 5 |
+
const month = date.getMonth() + 1;
|
| 6 |
+
const day = date.getDate();
|
| 7 |
+
return `${year}. ${month}. ${day}.`;
|
| 8 |
+
};
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
export const formatPoints = (points: number): string => {
|
| 11 |
+
return points.toLocaleString('ko-KR');
|
| 12 |
+
};
|
| 13 |
|
| 14 |
+
export const getContributionTypeLabel = (type: ContributionType): string => {
|
| 15 |
const labels: Record<ContributionType, string> = {
|
| 16 |
+
[ContributionType.POST]: '게시글 작성',
|
| 17 |
[ContributionType.COMMENT]: '댓글 작성',
|
| 18 |
+
[ContributionType.REVIEW]: '코드 리뷰',
|
| 19 |
[ContributionType.BUG_REPORT]: '버그 리포트',
|
| 20 |
+
[ContributionType.FEATURE]: '기능 제안',
|
| 21 |
+
[ContributionType.CODE]: '코드 기여',
|
| 22 |
};
|
| 23 |
return labels[type];
|
| 24 |
+
};
|
| 25 |
|
| 26 |
+
export const getLevelTitle = (level: number): string => {
|
| 27 |
+
if (level >= 30) return '레전드';
|
| 28 |
+
if (level >= 20) return '마스터';
|
| 29 |
+
if (level >= 10) return '고급자';
|
| 30 |
+
if (level >= 5) return '중급자';
|
| 31 |
+
return '초보자';
|
| 32 |
+
};
|
|
|
|
| 33 |
|
| 34 |
+
export const getRarityColor = (rarity: string): string => {
|
| 35 |
const colors: Record<string, string> = {
|
| 36 |
common: '#808080',
|
| 37 |
rare: '#4169E1',
|
| 38 |
epic: '#9932CC',
|
| 39 |
legendary: '#FFD700',
|
| 40 |
};
|
| 41 |
+
return colors[rarity] || '#000000';
|
| 42 |
+
};
|
| 43 |
|
| 44 |
+
export const calculateLevelProgress = (points: number): { current: number; next: number; progress: number } => {
|
| 45 |
+
const basePoints = 1000;
|
| 46 |
+
const currentLevel = Math.floor(points / basePoints) + 1;
|
| 47 |
+
const nextLevel = currentLevel + 1;
|
| 48 |
+
const currentLevelPoints = (currentLevel - 1) * basePoints;
|
| 49 |
+
const nextLevelPoints = currentLevel * basePoints;
|
| 50 |
const progress = ((points - currentLevelPoints) / (nextLevelPoints - currentLevelPoints)) * 100;
|
| 51 |
|
| 52 |
return {
|
| 53 |
current: currentLevel,
|
| 54 |
+
next: nextLevel,
|
| 55 |
+
progress: Math.min(100, Math.max(0, progress)),
|
| 56 |
};
|
| 57 |
+
};
|
| 58 |
|
| 59 |
export function generateRandomString(length: number): string {
|
| 60 |
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
src/setupTests.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import '@testing-library/jest-dom';
|
| 2 |
+
|
| 3 |
+
// 전역 테스트 설정
|
| 4 |
+
beforeAll(() => {
|
| 5 |
+
// 테스트 전에 실행할 설정
|
| 6 |
+
});
|
| 7 |
+
|
| 8 |
+
afterAll(() => {
|
| 9 |
+
// 테스트 후에 실행할 정리 작업
|
| 10 |
+
});
|
| 11 |
+
|
| 12 |
+
// 각 테스트 전후에 실행할 설정
|
| 13 |
+
beforeEach(() => {
|
| 14 |
+
// 각 테스트 전에 실행할 설정
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
afterEach(() => {
|
| 18 |
+
// 각 테스트 후에 실행할 정리 작업
|
| 19 |
+
});
|
src/types/incentive.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
export enum ContributionType {
|
| 2 |
-
|
| 3 |
COMMENT = 'COMMENT',
|
| 4 |
REVIEW = 'REVIEW',
|
| 5 |
BUG_REPORT = 'BUG_REPORT',
|
| 6 |
-
|
| 7 |
-
|
| 8 |
}
|
| 9 |
|
| 10 |
export enum ContributionStatus {
|
|
|
|
| 1 |
export enum ContributionType {
|
| 2 |
+
POST = 'POST',
|
| 3 |
COMMENT = 'COMMENT',
|
| 4 |
REVIEW = 'REVIEW',
|
| 5 |
BUG_REPORT = 'BUG_REPORT',
|
| 6 |
+
FEATURE = 'FEATURE',
|
| 7 |
+
CODE = 'CODE',
|
| 8 |
}
|
| 9 |
|
| 10 |
export enum ContributionStatus {
|