delta-sync
Version:
A lightweight framework for bi-directional database synchronization with automatic version tracking and conflict resolution.
241 lines (240 loc) • 10.3 kB
JavaScript
// tests/engine.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { SyncEngine } from '../core/SyncEngine';
import { MemoryAdapter } from '../core/adapters';
import { SyncStatus } from '../core/types';
describe('SyncEngine Tests', () => {
let engine;
let localAdapter;
let cloudAdapter;
let options;
beforeEach(() => {
localAdapter = new MemoryAdapter();
cloudAdapter = new MemoryAdapter();
options = {
autoSync: {
enabled: false,
pullInterval: 1000,
pushDebounce: 1000,
retryDelay: 500
},
onStatusUpdate: vi.fn(),
onChangePushed: vi.fn(),
onChangePulled: vi.fn(),
maxRetries: 3,
timeout: 5000,
batchSize: 100
};
engine = new SyncEngine(localAdapter, options);
});
describe('Initialization', () => {
it('should initialize correctly', async () => {
await engine.initialize();
const coordinator = await engine.getlocalCoordinator();
expect(coordinator).toBeDefined();
});
it('should set cloud adapter', async () => {
await engine.setCloudAdapter(cloudAdapter);
const adapter = await engine.getCloudAdapter();
expect(adapter).toBeDefined();
});
});
describe('Data Operations', () => {
beforeEach(async () => {
await engine.initialize();
});
it('should save single item', async () => {
const testItem = { id: 'test1', content: 'test content' };
const result = await engine.save('test_store', testItem);
expect(result).toHaveLength(1);
// 使用对象匹配器而不是严格相等
expect(result[0]).toMatchObject({
id: testItem.id,
content: testItem.content
});
// 确保存在 _ver 字段
expect(result[0]).toHaveProperty('_ver');
});
it('should save multiple items', async () => {
const testItems = [
{ id: 'test1', content: 'content 1' },
{ id: 'test2', content: 'content 2' }
];
const result = await engine.save('test_store', testItems);
expect(result).toHaveLength(2);
// 验证每个项目的基本属性
result.forEach((item, index) => {
expect(item).toMatchObject({
id: testItems[index].id,
content: testItems[index].content
});
});
});
it('should delete items', async () => {
const testItem = { id: 'delete-test', content: 'to be deleted' };
await engine.save('test_store', testItem);
await engine.delete('test_store', testItem.id);
const query = await engine.query('test_store');
expect(query.items).toHaveLength(0);
});
it('should query items with pagination', async () => {
const items = Array.from({ length: 50 }, (_, i) => ({
id: `item-${i}`,
content: `content ${i}`
}));
await engine.save('test_store', items);
const result = await engine.query('test_store', { limit: 20, offset: 0 });
expect(result.items).toHaveLength(20);
expect(result.hasMore).toBe(true);
});
});
describe('Sync Operations', () => {
beforeEach(async () => {
await engine.initialize();
await engine.setCloudAdapter(cloudAdapter);
});
it('should sync data between local and cloud', async () => {
// 准备本地数据
const localItems = [
{ id: 'local1', content: 'local content 1' },
{ id: 'local2', content: 'local content 2' }
];
await engine.save('test_store', localItems);
// 准备云端数据
const cloudItems = [
{ id: 'cloud1', content: 'cloud content 1' },
{ id: 'cloud2', content: 'cloud content 2' }
];
await cloudAdapter.putBulk('test_store', cloudItems);
// 执行同步
const result = await engine.sync();
expect(result.success).toBe(true);
expect(result.stats).toBeDefined();
// 验证数据同步结果
const localQuery = await engine.query('test_store');
expect(localQuery.items).toHaveLength(4); // 应该包含所有数据
});
it('should handle push operation', async () => {
const localItem = { id: 'push-test', content: 'push content' };
await engine.save('test_store', localItem);
const result = await engine.push();
expect(result.success).toBe(true);
expect(result.stats?.uploaded).toBeGreaterThan(0);
});
it('should handle pull operation', async () => {
const cloudItem = { id: 'pull-test', content: 'pull content' };
await cloudAdapter.putBulk('test_store', [cloudItem]);
const result = await engine.pull();
expect(result.success).toBe(true);
expect(result.stats?.downloaded).toBeGreaterThan(0);
});
});
describe('Error Handling', () => {
it('should handle sync without cloud adapter', async () => {
const result = await engine.sync();
expect(result.success).toBe(false);
expect(result.error).toBe('Cloud adapter not set');
});
it('should handle operation failures', async () => {
// 模拟操作失败
const failingAdapter = new MemoryAdapter();
vi.spyOn(failingAdapter, 'putBulk').mockRejectedValue(new Error('Operation failed'));
const engineWithFailingAdapter = new SyncEngine(failingAdapter, options);
await engineWithFailingAdapter.initialize();
try {
await engineWithFailingAdapter.save('test_store', { id: 'fail-test', content: 'test' });
}
catch (error) {
expect(error).toBeDefined();
}
});
});
describe('Status Management', () => {
it('should update sync status correctly', async () => {
await engine.initialize();
await engine.setCloudAdapter(cloudAdapter);
// 准备测试数据以确保有同步操作发生
const testItem = { id: 'status-test', content: 'test content' };
await engine.save('test_store', testItem);
// 确保使用正确的 mock 函数
const statusUpdateFn = vi.fn();
engine.updateSyncOptions({
onStatusUpdate: statusUpdateFn
});
// 执行同步
await engine.sync();
// 验证状态更新回调
expect(statusUpdateFn).toHaveBeenCalled();
// 验证状态序列
expect(statusUpdateFn).toHaveBeenCalledWith(expect.any(Number));
// 验证最终状态
const lastCall = statusUpdateFn.mock.calls[statusUpdateFn.mock.calls.length - 1];
expect(lastCall[0]).toBe(SyncStatus.IDLE);
});
it('should handle offline status', async () => {
await engine.initialize();
await engine.setCloudAdapter(cloudAdapter);
const statusUpdateFn = vi.fn();
engine.updateSyncOptions({
onStatusUpdate: statusUpdateFn
});
// 断开连接
engine.disconnectCloud();
// 等待一个微任务周期
await Promise.resolve();
// 验证是否调用了状态更新
expect(statusUpdateFn).toHaveBeenCalledWith(SyncStatus.OFFLINE);
// 清除之前的调用记录
statusUpdateFn.mockClear();
// 尝试同步
const syncResult = await engine.sync();
// 验证同步结果
expect(syncResult.success).toBe(false);
expect(syncResult.error).toBe('Cloud adapter not set');
// 验证状态更新被再次调用
expect(statusUpdateFn).toHaveBeenCalledWith(SyncStatus.OFFLINE);
});
it('should update status during sync operations', async () => {
await engine.initialize();
await engine.setCloudAdapter(cloudAdapter);
// 准备测试数据
const localItem = { id: 'sync-status-test', content: 'test content' };
await engine.save('test_store', localItem);
// 设置状态监听
const statusUpdateFn = vi.fn();
engine.updateSyncOptions({
onStatusUpdate: statusUpdateFn
});
// 执行推送操作
await engine.push();
// 验证状态变化序列
expect(statusUpdateFn).toHaveBeenCalled();
const statusCalls = statusUpdateFn.mock.calls.map(call => call[0]);
expect(statusCalls).toContain(SyncStatus.UPLOADING);
expect(statusCalls).toContain(SyncStatus.IDLE);
});
});
describe('Change Notifications', () => {
it('should notify on data changes', async () => {
await engine.initialize();
await engine.setCloudAdapter(cloudAdapter);
const onChangePushed = options.onChangePushed;
// 触发数据变更
await engine.save('test_store', { id: 'change-test', content: 'test' });
await engine.push();
expect(onChangePushed).toHaveBeenCalled();
const changeSet = onChangePushed.mock.calls[0][0];
expect(changeSet.put.size).toBeGreaterThan(0);
});
});
describe('Cleanup', () => {
it('should dispose resources correctly', async () => {
await engine.initialize();
engine.enableAutoSync();
engine.dispose();
// 验证清理后的状态
const cloudAdapter = await engine.getCloudAdapter();
expect(cloudAdapter).toBeUndefined();
});
});
});