feat: add QR code login and enhance chat experience
- Add QR code login flow with scanner and confirmation screens - Implement swipe-to-reply in chat with SwipeableMessageBubble - Replace FlatList with FlashList for chat performance optimization - Add Schedule stack navigator with CourseDetail screen support - Configure deep linking for QR code login (carrotbbs://qrcode/login/:sessionId) - Update branding from 胡萝卜 to 萝卜社区 - Display dynamic app version in settings
This commit is contained in:
@@ -1,287 +0,0 @@
|
||||
/**
|
||||
* 乐观更新工具函数测试
|
||||
*/
|
||||
|
||||
import { optimisticUpdate, simpleOptimisticUpdate, createOptimisticUpdaterFactory } from '../optimisticUpdate';
|
||||
|
||||
// 模拟数据
|
||||
interface TestState {
|
||||
posts: Array<{ id: string; likes: number; isLiked: boolean }>;
|
||||
users: Array<{ id: string; isFollowing: boolean }>;
|
||||
}
|
||||
|
||||
const initialState: TestState = {
|
||||
posts: [
|
||||
{ id: '1', likes: 10, isLiked: false },
|
||||
{ id: '2', likes: 5, isLiked: true },
|
||||
],
|
||||
users: [
|
||||
{ id: 'user1', isFollowing: false },
|
||||
{ id: 'user2', isFollowing: true },
|
||||
],
|
||||
};
|
||||
|
||||
describe('optimisticUpdate', () => {
|
||||
let currentState: TestState;
|
||||
|
||||
beforeEach(() => {
|
||||
currentState = JSON.parse(JSON.stringify(initialState));
|
||||
});
|
||||
|
||||
describe('成功场景', () => {
|
||||
it('应该执行乐观更新并在API成功后保持更新', async () => {
|
||||
const mockApiCall = jest.fn().mockResolvedValue({ success: true, likes: 11 });
|
||||
|
||||
const result = await optimisticUpdate<TestState, { success: boolean; likes: number }>({
|
||||
getState: () => currentState,
|
||||
optimisticUpdate: (state) => ({
|
||||
...state,
|
||||
posts: state.posts.map((p) =>
|
||||
p.id === '1' ? { ...p, likes: p.likes + 1, isLiked: true } : p
|
||||
),
|
||||
}),
|
||||
apiCall: mockApiCall,
|
||||
onSuccess: (result, current) => ({
|
||||
...current,
|
||||
posts: current.posts.map((p) =>
|
||||
p.id === '1' ? { ...p, likes: result.likes } : p
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
// 验证API被调用
|
||||
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
||||
// 验证返回结果
|
||||
expect(result).toEqual({ success: true, likes: 11 });
|
||||
});
|
||||
|
||||
it('应该在乐观更新后立即改变状态', async () => {
|
||||
const mockApiCall = jest.fn().mockImplementation(() => {
|
||||
// 在API调用期间验证状态已被乐观更新
|
||||
expect(currentState.posts[0].isLiked).toBe(true);
|
||||
expect(currentState.posts[0].likes).toBe(11);
|
||||
return Promise.resolve({ success: true });
|
||||
});
|
||||
|
||||
await optimisticUpdate<TestState, any>({
|
||||
getState: () => currentState,
|
||||
optimisticUpdate: (state) => {
|
||||
const newState = {
|
||||
...state,
|
||||
posts: state.posts.map((p) =>
|
||||
p.id === '1' ? { ...p, likes: p.likes + 1, isLiked: true } : p
|
||||
),
|
||||
};
|
||||
currentState = newState; // 模拟状态更新
|
||||
return newState;
|
||||
},
|
||||
apiCall: mockApiCall,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('失败回滚场景', () => {
|
||||
it('应该在API失败时回滚到原始状态', async () => {
|
||||
const mockApiCall = jest.fn().mockRejectedValue(new Error('Network error'));
|
||||
const onError = jest.fn((error, original) => original);
|
||||
|
||||
const result = await optimisticUpdate<TestState, any>({
|
||||
getState: () => currentState,
|
||||
optimisticUpdate: (state) => ({
|
||||
...state,
|
||||
posts: state.posts.map((p) =>
|
||||
p.id === '1' ? { ...p, likes: p.likes + 1, isLiked: true } : p
|
||||
),
|
||||
}),
|
||||
apiCall: mockApiCall,
|
||||
onError,
|
||||
errorMessage: '点赞失败',
|
||||
});
|
||||
|
||||
// 验证API被调用
|
||||
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
||||
// 验证onError被调用
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect(onError).toHaveBeenCalledWith(
|
||||
expect.any(Error),
|
||||
initialState
|
||||
);
|
||||
// 验证返回undefined(因为失败了)
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该在静默模式下不抛出错误', async () => {
|
||||
const mockApiCall = jest.fn().mockRejectedValue(new Error('Network error'));
|
||||
|
||||
// 不应该抛出错误
|
||||
await expect(
|
||||
optimisticUpdate<TestState, any>({
|
||||
getState: () => currentState,
|
||||
optimisticUpdate: (state) => state,
|
||||
apiCall: mockApiCall,
|
||||
silent: true,
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该在非静默模式下抛出错误', async () => {
|
||||
const mockApiCall = jest.fn().mockRejectedValue(new Error('Network error'));
|
||||
|
||||
// 应该抛出错误
|
||||
await expect(
|
||||
optimisticUpdate<TestState, any>({
|
||||
getState: () => currentState,
|
||||
optimisticUpdate: (state) => state,
|
||||
apiCall: mockApiCall,
|
||||
silent: false,
|
||||
})
|
||||
).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('应该记录错误日志', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
const mockApiCall = jest.fn().mockRejectedValue(new Error('API Error'));
|
||||
|
||||
await optimisticUpdate<TestState, any>({
|
||||
getState: () => currentState,
|
||||
optimisticUpdate: (state) => state,
|
||||
apiCall: mockApiCall,
|
||||
errorMessage: '自定义错误消息',
|
||||
silent: true,
|
||||
});
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('自定义错误消息:', expect.any(Error));
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('simpleOptimisticUpdate', () => {
|
||||
let currentState: TestState;
|
||||
let setStateMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
currentState = JSON.parse(JSON.stringify(initialState));
|
||||
setStateMock = jest.fn((newState) => {
|
||||
currentState = newState;
|
||||
});
|
||||
});
|
||||
|
||||
it('应该成功执行乐观更新', async () => {
|
||||
const mockApiCall = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
await simpleOptimisticUpdate<TestState>({
|
||||
getState: () => currentState,
|
||||
setState: setStateMock,
|
||||
optimisticUpdate: (state) => ({
|
||||
...state,
|
||||
posts: state.posts.map((p) =>
|
||||
p.id === '1' ? { ...p, likes: p.likes + 1 } : p
|
||||
),
|
||||
}),
|
||||
rollbackState: (original) => original,
|
||||
apiCall: mockApiCall,
|
||||
});
|
||||
|
||||
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
||||
expect(setStateMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('应该在失败时回滚状态', async () => {
|
||||
const mockApiCall = jest.fn().mockRejectedValue(new Error('Error'));
|
||||
const rollbackMock = jest.fn((original) => original);
|
||||
|
||||
await simpleOptimisticUpdate<TestState>({
|
||||
getState: () => currentState,
|
||||
setState: setStateMock,
|
||||
optimisticUpdate: (state) => ({
|
||||
...state,
|
||||
posts: state.posts.map((p) =>
|
||||
p.id === '1' ? { ...p, likes: p.likes + 1 } : p
|
||||
),
|
||||
}),
|
||||
rollbackState: rollbackMock,
|
||||
apiCall: mockApiCall,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
// 验证乐观更新和回滚都被调用
|
||||
expect(setStateMock).toHaveBeenCalledTimes(2);
|
||||
expect(rollbackMock).toHaveBeenCalledWith(initialState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createOptimisticUpdaterFactory', () => {
|
||||
interface StoreState {
|
||||
posts: Array<{ id: string; likes: number }>;
|
||||
count: number;
|
||||
}
|
||||
|
||||
let storeState: StoreState;
|
||||
let setMock: jest.Mock;
|
||||
let getMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
storeState = {
|
||||
posts: [{ id: '1', likes: 10 }],
|
||||
count: 0,
|
||||
};
|
||||
setMock = jest.fn((fn) => {
|
||||
storeState = fn(storeState);
|
||||
});
|
||||
getMock = jest.fn(() => storeState);
|
||||
});
|
||||
|
||||
it('应该创建乐观更新器并正常工作', async () => {
|
||||
const optimisticUpdater = createOptimisticUpdaterFactory<StoreState>(setMock, getMock);
|
||||
const mockApiCall = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
await optimisticUpdater({
|
||||
key: 'posts',
|
||||
optimisticUpdate: (posts) =>
|
||||
posts.map((p) => (p.id === '1' ? { ...p, likes: p.likes + 1 } : p)),
|
||||
rollbackState: (original) => original,
|
||||
apiCall: mockApiCall,
|
||||
});
|
||||
|
||||
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
||||
expect(setMock).toHaveBeenCalledTimes(1);
|
||||
expect(storeState.posts[0].likes).toBe(11);
|
||||
});
|
||||
|
||||
it('应该在API失败时回滚状态', async () => {
|
||||
const optimisticUpdater = createOptimisticUpdaterFactory<StoreState>(setMock, getMock);
|
||||
const mockApiCall = jest.fn().mockRejectedValue(new Error('Error'));
|
||||
|
||||
await optimisticUpdater({
|
||||
key: 'posts',
|
||||
optimisticUpdate: (posts) =>
|
||||
posts.map((p) => (p.id === '1' ? { ...p, likes: p.likes + 1 } : p)),
|
||||
rollbackState: (original) => original,
|
||||
apiCall: mockApiCall,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
// 乐观更新 + 回滚 = 2次set调用
|
||||
expect(setMock).toHaveBeenCalledTimes(2);
|
||||
// 最终状态应该和初始状态一样
|
||||
expect(storeState.posts[0].likes).toBe(10);
|
||||
});
|
||||
|
||||
it('应该处理不同的state key', async () => {
|
||||
const optimisticUpdater = createOptimisticUpdaterFactory<StoreState>(setMock, getMock);
|
||||
const mockApiCall = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
await optimisticUpdater({
|
||||
key: 'count',
|
||||
optimisticUpdate: (count) => count + 1,
|
||||
rollbackState: (original) => original,
|
||||
apiCall: mockApiCall,
|
||||
});
|
||||
|
||||
expect(storeState.count).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user