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:
lafay
2026-03-20 19:28:42 +08:00
parent 59877e6ae3
commit a005fb0a15
24 changed files with 2878 additions and 1859 deletions

View File

@@ -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);
});
});