修改页面
CI / Server (Go) (push) Has been cancelled
CI / Web (Next.js) (push) Has been cancelled
CI / Mobile (React Native) (push) Has been cancelled
CI / Admin (React) (push) Has been cancelled
CI / Docker Build (push) Has been cancelled

This commit is contained in:
zeronline
2026-06-25 16:33:32 +08:00
parent c5cccd346b
commit 545329ac40
63 changed files with 2560 additions and 161 deletions
+198
View File
@@ -0,0 +1,198 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
// Mock fetch
const mockFetch = vi.fn()
global.fetch = mockFetch
// Mock localStorage
const store: Record<string, string> = {}
vi.stubGlobal('localStorage', {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => { store[key] = value },
removeItem: (key: string) => { delete store[key] },
})
beforeEach(() => {
mockFetch.mockReset()
Object.keys(store).forEach((k) => delete store[k])
})
function mockResponse(data: any) {
return { json: () => Promise.resolve(data) }
}
describe('Api Service', () => {
it('should send GET request with token', async () => {
store['token'] = 'test-token'
mockFetch.mockResolvedValue(mockResponse({ code: 0, data: { id: '1' } }))
const { request } = await import('../services/api')
const result = await request('/api/test')
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8080/api/test',
expect.objectContaining({
headers: expect.objectContaining({ Authorization: 'Bearer test-token' }),
}),
)
expect(result).toEqual({ id: '1' })
})
it('should throw on non-zero code', async () => {
mockFetch.mockResolvedValue(mockResponse({ code: 400, message: 'Bad Request' }))
const { request } = await import('../services/api')
await expect(request('/api/test')).rejects.toThrow('Bad Request')
})
it('should support POST method', async () => {
mockFetch.mockResolvedValue(mockResponse({ code: 0, data: { created: true } }))
const { api } = await import('../services/api')
const result = await api.post('/api/items', { name: 'test' })
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8080/api/items',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ name: 'test' }),
}),
)
expect(result).toEqual({ created: true })
})
it('should support PUT and DELETE methods', async () => {
mockFetch.mockResolvedValue(mockResponse({ code: 0, data: null }))
const { api } = await import('../services/api')
await api.put('/api/items/1', { name: 'updated' })
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8080/api/items/1',
expect.objectContaining({ method: 'PUT' }),
)
await api.delete('/api/items/1')
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8080/api/items/1',
expect.objectContaining({ method: 'DELETE' }),
)
})
})
describe('Auth Service', () => {
it('should store and clear token', async () => {
const { setToken, clearToken, getStoredUser } = await import('../services/api')
setToken('abc')
expect(store['token']).toBe('abc')
setStoredUser({ nickname: 'Test' })
expect(getStoredUser()).toEqual({ nickname: 'Test' })
clearToken()
expect(store['token']).toBeUndefined()
})
// Helper to test the import below
const { setStoredUser } = await import('../services/api')
void setStoredUser
})
describe('CheckIn Service', () => {
it('should call createItem with proper payload', async () => {
mockFetch.mockResolvedValue(mockResponse({ code: 0, data: { id: '1', name: 'Read' } }))
const { createItem } = await import('../services/checkin')
const result = await createItem({ name: 'Read', category: 'study' })
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8080/api/checkin/items',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
name: 'Read', category: 'study', icon: undefined,
dailyGoal: undefined, cycleType: 'daily', privacy: 2,
}),
}),
)
expect(result.name).toBe('Read')
})
it('should list items', async () => {
mockFetch.mockResolvedValue(mockResponse({ code: 0, data: [{ id: '1' }, { id: '2' }] }))
const { listItems } = await import('../services/checkin')
const items = await listItems()
expect(items).toHaveLength(2)
})
it('should get streak', async () => {
mockFetch.mockResolvedValue(mockResponse({ code: 0, data: { itemId: '1', streak: 7 } }))
const { getStreak } = await import('../services/checkin')
const result = await getStreak('1')
expect(result.streak).toBe(7)
})
it('should get today status', async () => {
mockFetch.mockResolvedValue(mockResponse({ code: 0, data: { 'item-1': true, 'item-2': false } }))
const { getTodayStatus } = await import('../services/checkin')
const status = await getTodayStatus()
expect(status['item-1']).toBe(true)
expect(status['item-2']).toBe(false)
})
})
describe('Social Service', () => {
it('should create and fetch feed', async () => {
mockFetch.mockResolvedValue(mockResponse({ code: 0, data: { list: [{ id: 'p1' }], total: 1 } }))
const { getFeed } = await import('../services/social')
const feed = await getFeed()
expect(feed.list).toHaveLength(1)
expect(feed.total).toBe(1)
})
it('should send friend request', async () => {
mockFetch.mockResolvedValue(mockResponse({ code: 0, data: null }))
const { sendFriendRequest } = await import('../services/social')
await sendFriendRequest('user-2')
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8080/api/social/friends/request',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ friendId: 'user-2' }),
}),
)
})
})
describe('Shared Utils', () => {
it('formatDate should return YYYY-MM-DD', async () => {
const { formatDate } = await import('../utils/index')
expect(formatDate('2026-06-25')).toBe('2026-06-25')
expect(formatDate(new Date('2026-01-05'))).toBe('2026-01-05')
})
it('timeAgo should return relative time', async () => {
const { timeAgo } = await import('../utils/index')
const now = new Date().toISOString()
expect(timeAgo(now)).toBe('刚刚')
})
it('getCategoryLabel should return Chinese label', async () => {
const { getCategoryLabel } = await import('../utils/index')
expect(getCategoryLabel('study')).toBe('学习成长')
expect(getCategoryLabel('health')).toBe('健康生活')
expect(getCategoryLabel('unknown')).toBe('unknown')
})
it('formatPoints should format with + sign', async () => {
const { formatPoints } = await import('../utils/index')
expect(formatPoints(10)).toBe('+10')
expect(formatPoints(0)).toBe('0')
expect(formatPoints(null)).toBe('--')
})
})
+64
View File
@@ -0,0 +1,64 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
const mockRequest = vi.fn()
vi.mock('../services/api', () => ({
request: mockRequest,
api: {
get: (path: string) => mockRequest('get', path),
post: (path: string, data?: unknown) => mockRequest('post', path, data),
put: (path: string, data?: unknown) => mockRequest('put', path, data),
delete: (path: string) => mockRequest('delete', path),
},
}))
describe('CheckIn Service', () => {
beforeEach(() => {
mockRequest.mockReset()
})
it('should call createItem API', async () => {
mockRequest.mockResolvedValue({ id: '1', name: 'Daily Read' })
const { createItem } = await import('../services/checkin')
const result = await createItem({ name: 'Daily Read', category: 'study' })
expect(mockRequest).toHaveBeenCalledWith('post', '/api/checkin/items', {
name: 'Daily Read',
category: 'study',
icon: undefined,
dailyGoal: undefined,
cycleType: 'daily',
privacy: 2,
})
expect(result).toEqual({ id: '1', name: 'Daily Read' })
})
it('should call listItems API', async () => {
mockRequest.mockResolvedValue([{ id: '1' }])
const { listItems } = await import('../services/checkin')
const result = await listItems()
expect(mockRequest).toHaveBeenCalledWith('get', '/api/checkin/items')
expect(result).toHaveLength(1)
})
it('should call getStreak API', async () => {
mockRequest.mockResolvedValue({ itemId: '1', streak: 7 })
const { getStreak } = await import('../services/checkin')
const result = await getStreak('1')
expect(mockRequest).toHaveBeenCalledWith('get', '/api/checkin/streak/1')
expect(result.streak).toBe(7)
})
})
describe('Social Service', () => {
beforeEach(() => {
mockRequest.mockReset()
})
it('should call getFeed API', async () => {
mockRequest.mockResolvedValue({ list: [{ id: 'p1' }], total: 1, page: 1 })
const { getFeed } = await import('../services/social')
const result = await getFeed(1, 20)
expect(mockRequest).toHaveBeenCalledWith('get', '/api/social/feed?page=1&pageSize=20')
expect(result.list).toHaveLength(1)
})
})
+14
View File
@@ -0,0 +1,14 @@
export {
formatDate,
formatMonth,
timeAgo,
CHECKIN_CATEGORY,
getCategoryLabel,
getCycleLabel,
getPrivacyLabel,
formatPoints,
truncate,
shortId,
CYCLE_LABEL,
} from '../../shared/utils'
export type { CheckinCategory } from '../../shared/utils'
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from 'vitest/config'
import path from 'path'
export default defineConfig({
test: {
environment: 'node',
globals: true,
include: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
setupFiles: [],
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
})