Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/hooks/useDebounce.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useDebouncedInput } from '@/hooks/useDebounce';

describe('useDebouncedInput', () => {
beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.useRealTimers();
});

it('지연 시간 전에는 setState가 호출되지 않는다', () => {
const setState = vi.fn();
const { result } = renderHook(() => useDebouncedInput(setState, 300));

act(() => {
result.current('name', '홍길동');
});

expect(setState).not.toHaveBeenCalled();
});

it('지연 시간 후에 setState가 호출된다', () => {
const setState = vi.fn();
const { result } = renderHook(() => useDebouncedInput(setState, 300));

act(() => {
result.current('name', '홍길동');
vi.advanceTimersByTime(300);
});

expect(setState).toHaveBeenCalledTimes(1);
});

it('연속 호출 시 마지막 호출만 실행된다', () => {
const setState = vi.fn();
const { result } = renderHook(() => useDebouncedInput(setState, 300));

act(() => {
result.current('name', '첫번째');
result.current('name', '두번째');
result.current('name', '세번째');
vi.advanceTimersByTime(300);
});

expect(setState).toHaveBeenCalledTimes(1);
});

it('setState에 이전 상태를 기반으로 업데이트 함수를 전달한다', () => {
const setState = vi.fn();
const { result } = renderHook(() => useDebouncedInput(setState, 300));

act(() => {
result.current('email', 'test@example.com');
vi.advanceTimersByTime(300);
});

expect(setState).toHaveBeenCalledWith(expect.any(Function));

const updater = setState.mock.calls[0][0];
const prevState = { name: '홍길동' };
expect(updater(prevState)).toEqual({ name: '홍길동', email: 'test@example.com' });
});

it('서로 다른 필드를 각각 디바운싱할 수 있다', () => {
const setState = vi.fn();
const { result } = renderHook(() => useDebouncedInput(setState, 300));

act(() => {
result.current('title', '제목');
vi.advanceTimersByTime(300);
});

act(() => {
result.current('content', '내용');
vi.advanceTimersByTime(300);
});

expect(setState).toHaveBeenCalledTimes(2);
});
});
34 changes: 34 additions & 0 deletions src/utils/calc.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { itemIdxByPage } from '@/utils/calc';

describe('itemIdxByPage', () => {
it('첫 페이지 첫 번째 항목의 역순 인덱스를 반환한다', () => {
// resultCnt=10, 1페이지, pageSize=5, index=0 → 10+1-(0*5+0+1) = 10
expect(itemIdxByPage(10, 1, 5, 0)).toBe(10);
});

it('첫 페이지 마지막 항목의 역순 인덱스를 반환한다', () => {
// resultCnt=10, 1페이지, pageSize=5, index=4 → 10+1-(0*5+4+1) = 6
expect(itemIdxByPage(10, 1, 5, 4)).toBe(6);
});

it('두 번째 페이지 첫 번째 항목의 역순 인덱스를 반환한다', () => {
// resultCnt=10, 2페이지, pageSize=5, index=0 → 10+1-(1*5+0+1) = 5
expect(itemIdxByPage(10, 2, 5, 0)).toBe(5);
});

it('두 번째 페이지 마지막 항목의 역순 인덱스를 반환한다', () => {
// resultCnt=10, 2페이지, pageSize=5, index=4 → 10+1-(1*5+4+1) = 1
expect(itemIdxByPage(10, 2, 5, 4)).toBe(1);
});

it('resultCnt가 0이면 음수 인덱스를 반환할 수 있다', () => {
// resultCnt=0, 1페이지, pageSize=5, index=0 → 0+1-(0+0+1) = 0
expect(itemIdxByPage(0, 1, 5, 0)).toBe(0);
});

it('pageSize=10인 경우도 올바르게 동작한다', () => {
// resultCnt=25, 3페이지, pageSize=10, index=2 → 25+1-(2*10+2+1) = 3
expect(itemIdxByPage(25, 3, 10, 2)).toBe(3);
});
});
35 changes: 35 additions & 0 deletions src/utils/passwordHash.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, it, expect } from 'vitest';
import { hashPassword } from '@/utils/passwordHash';

describe('hashPassword', () => {
it('동일한 id와 password는 동일한 해시를 반환한다', async () => {
const hash1 = await hashPassword('user01', 'password123');
const hash2 = await hashPassword('user01', 'password123');
expect(hash1).toBe(hash2);
});

it('Base64 문자열을 반환한다', async () => {
const hash = await hashPassword('user01', 'password123');
expect(typeof hash).toBe('string');
// Base64 형식 검증 (알파벳, 숫자, +, /, = 만 허용)
expect(hash).toMatch(/^[A-Za-z0-9+/]+=*$/);
});

it('password가 달라지면 다른 해시를 반환한다', async () => {
const hash1 = await hashPassword('user01', 'password123');
const hash2 = await hashPassword('user01', 'differentPassword');
expect(hash1).not.toBe(hash2);
});

it('id가 달라지면 다른 해시를 반환한다', async () => {
const hash1 = await hashPassword('user01', 'password123');
const hash2 = await hashPassword('user02', 'password123');
expect(hash1).not.toBe(hash2);
});

it('SHA-256 기반 Base64 결과는 44자이다', async () => {
const hash = await hashPassword('user01', 'password123');
// SHA-256은 32바이트 → Base64 인코딩 시 44자 (패딩 포함)
expect(hash.length).toBe(44);
});
});
75 changes: 75 additions & 0 deletions src/utils/storage.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
getLocalItem,
setLocalItem,
removeLocalItem,
getSessionItem,
setSessionItem,
removeSessionItem,
} from '@/utils/storage';

function createStorageMock() {
let store = {};
return {
getItem: (key) => store[key] ?? null,
setItem: (key, value) => { store[key] = String(value); },
removeItem: (key) => { delete store[key]; },
clear: () => { store = {}; },
};
}

describe('storage utils', () => {
beforeEach(() => {
vi.stubGlobal('localStorage', createStorageMock());
vi.stubGlobal('sessionStorage', createStorageMock());
});

describe('localStorage', () => {
it('값을 저장하고 가져온다', () => {
setLocalItem('user', { id: 1, name: '홍길동' });
expect(getLocalItem('user')).toEqual({ id: 1, name: '홍길동' });
});

it('존재하지 않는 키는 null을 반환한다', () => {
expect(getLocalItem('missing')).toBeNull();
});

it('항목을 삭제하면 null을 반환한다', () => {
setLocalItem('token', 'abc');
removeLocalItem('token');
expect(getLocalItem('token')).toBeNull();
});

it('문자열 값을 저장하고 가져온다', () => {
setLocalItem('lang', 'ko');
expect(getLocalItem('lang')).toBe('ko');
});

it('숫자 값을 저장하고 가져온다', () => {
setLocalItem('count', 42);
expect(getLocalItem('count')).toBe(42);
});

it('undefined를 저장하면 null로 직렬화된다', () => {
setLocalItem('empty', undefined);
expect(getLocalItem('empty')).toBeNull();
});
});

describe('sessionStorage', () => {
it('값을 저장하고 가져온다', () => {
setSessionItem('session', { token: 'xyz' });
expect(getSessionItem('session')).toEqual({ token: 'xyz' });
});

it('존재하지 않는 키는 null을 반환한다', () => {
expect(getSessionItem('missing')).toBeNull();
});

it('항목을 삭제하면 null을 반환한다', () => {
setSessionItem('state', 'active');
removeSessionItem('state');
expect(getSessionItem('state')).toBeNull();
});
});
});