202 lines
6.5 KiB
TypeScript
202 lines
6.5 KiB
TypeScript
/**
|
|
* Vue 集成 composables 测试(v0.2.6 补强)
|
|
* @module tests/integrations/vue
|
|
*
|
|
* 项目零运行时依赖(vue 为 peer dependency),
|
|
* 使用最小 Vue mock 验证 composables 的真实逻辑:
|
|
* - onMounted 时执行查询
|
|
* - watch sql/deps 变化重新执行
|
|
* - refresh 手动刷新
|
|
* - 表名校验(SQL 注入防护)
|
|
*/
|
|
|
|
// ---- 最小 Vue mock(自包含:jest.mock 工厂不能引用外部变量) ----
|
|
jest.mock('vue', () => {
|
|
const mountQueue: Array<() => void | Promise<void>> = [];
|
|
const unmountQueue: Array<() => void | Promise<void>> = [];
|
|
const watchList: Array<{ sources: any[]; cb: () => void | Promise<void> }> = [];
|
|
return {
|
|
ref: (init: any) => {
|
|
const box: { value: any } = { value: init };
|
|
return box;
|
|
},
|
|
watch: (sources: any[], cb: any) => {
|
|
watchList.push({ sources, cb });
|
|
},
|
|
onMounted: (fn: any) => {
|
|
mountQueue.push(fn);
|
|
},
|
|
onUnmounted: (fn: any) => {
|
|
unmountQueue.push(fn);
|
|
},
|
|
__mockMounted: mountQueue,
|
|
__mockUnmounted: unmountQueue,
|
|
__mockWatch: watchList,
|
|
__mockReset: () => {
|
|
mountQueue.length = 0;
|
|
unmountQueue.length = 0;
|
|
watchList.length = 0;
|
|
},
|
|
};
|
|
}, { virtual: true });
|
|
|
|
import { useSqlarkQuery, useSqlarkTable, useSqlarkDatabase } from '../../src/integrations/vue';
|
|
|
|
/** mock 模块内的挂载/监听队列(自包含作用域) */
|
|
const vueMock = jest.requireMock('vue') as {
|
|
__mockMounted: Array<() => void | Promise<void>>;
|
|
__mockUnmounted: Array<() => void | Promise<void>>;
|
|
__mockWatch: Array<{ sources: any[]; cb: () => void | Promise<void> }>;
|
|
__mockReset: () => void;
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vueMock.__mockReset();
|
|
});
|
|
|
|
/** 模拟组件挂载:执行 onMounted 注册的回调 */
|
|
async function flushMounted(): Promise<void> {
|
|
const fns = vueMock.__mockMounted.splice(0);
|
|
for (const fn of fns) {
|
|
await fn();
|
|
}
|
|
}
|
|
|
|
/** 模拟组件卸载:执行 onUnmounted 注册的回调 */
|
|
async function flushUnmounted(): Promise<void> {
|
|
const fns = vueMock.__mockUnmounted.splice(0);
|
|
for (const fn of fns) {
|
|
await fn();
|
|
}
|
|
}
|
|
|
|
/** 触发 watch 回调 */
|
|
async function flushWatch(): Promise<void> {
|
|
const pairs = vueMock.__mockWatch.splice(0);
|
|
for (const { cb } of pairs) {
|
|
await cb();
|
|
}
|
|
}
|
|
|
|
/** 构造最小响应式引用(与 vue.mock 的 ref 等价) */
|
|
function makeRef<T>(init: T): { value: T } {
|
|
return { value: init };
|
|
}
|
|
|
|
describe('useSqlarkQuery', () => {
|
|
test('onMounted 时执行 SQL 查询并更新响应式 data', async () => {
|
|
const db = { query: jest.fn().mockResolvedValue([{ id: '1' }]) } as any;
|
|
const hook = useSqlarkQuery(db, 'SELECT * FROM users');
|
|
|
|
expect(hook.loading.value).toBe(true);
|
|
expect(db.query).not.toHaveBeenCalled();
|
|
|
|
await flushMounted();
|
|
|
|
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
|
|
expect(hook.loading.value).toBe(false);
|
|
expect(hook.data.value).toEqual([{ id: '1' }]);
|
|
expect(hook.error.value).toBeNull();
|
|
});
|
|
|
|
test('查询失败时设置 error', async () => {
|
|
const db = { query: jest.fn().mockRejectedValue(new Error('vue boom')) } as any;
|
|
const hook = useSqlarkQuery(db, 'SELECT * FROM users');
|
|
|
|
await flushMounted();
|
|
|
|
expect(hook.error.value).toBeInstanceOf(Error);
|
|
expect((hook.error.value as Error).message).toBe('vue boom');
|
|
expect(hook.data.value).toEqual([]);
|
|
});
|
|
|
|
test('refresh 重新执行查询', async () => {
|
|
let call = 0;
|
|
const db = { query: jest.fn().mockImplementation(async () => [{ n: ++call }]) } as any;
|
|
const hook = useSqlarkQuery(db, 'SELECT * FROM users');
|
|
|
|
await flushMounted();
|
|
expect(hook.data.value).toEqual([{ n: 1 }]);
|
|
|
|
await hook.refresh();
|
|
expect(db.query).toHaveBeenCalledTimes(2);
|
|
expect(hook.data.value).toEqual([{ n: 2 }]);
|
|
});
|
|
|
|
test('watch 注册在 sql 与 deps 上', async () => {
|
|
const db = { query: jest.fn().mockResolvedValue([]) } as any;
|
|
const sqlRef = makeRef('SELECT * FROM users');
|
|
useSqlarkQuery(db, sqlRef.value, [sqlRef]);
|
|
|
|
expect(vueMock.__mockWatch).toHaveLength(1);
|
|
expect(vueMock.__mockWatch[0].sources).toHaveLength(2); // [() => sql, ...deps]
|
|
|
|
// 模拟依赖变化触发 watch
|
|
sqlRef.value = 'SELECT * FROM orders';
|
|
await flushWatch();
|
|
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
|
|
});
|
|
});
|
|
|
|
describe('useSqlarkTable', () => {
|
|
test('合法表名执行全表查询', async () => {
|
|
const db = { query: jest.fn().mockResolvedValue([{ id: 1 }]) } as any;
|
|
const hook = useSqlarkTable(db, 'users');
|
|
|
|
await flushMounted();
|
|
|
|
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
|
|
expect(hook.data.value).toEqual([{ id: 1 }]);
|
|
});
|
|
|
|
test('非法表名抛出校验错误(SQL 注入防护)', () => {
|
|
const db = { query: jest.fn() } as any;
|
|
expect(() => useSqlarkTable(db, 'users; DELETE FROM orders')).toThrow(/Invalid table name/);
|
|
expect(() => useSqlarkTable(db, 'users--')).toThrow(/Invalid table name/);
|
|
expect(db.query).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('useSqlarkDatabase', () => {
|
|
test('onMounted 创建并初始化数据库实例', async () => {
|
|
const hook = useSqlarkDatabase({ name: 'vue-hook' });
|
|
expect(hook.ready.value).toBe(false);
|
|
|
|
await flushMounted();
|
|
|
|
expect(hook.db.value).not.toBeNull();
|
|
expect(hook.ready.value).toBe(true);
|
|
expect(hook.error.value).toBeNull();
|
|
});
|
|
|
|
test('初始化失败时设置 error', async () => {
|
|
// 使用非法配置触发初始化错误(mode 未知)
|
|
const hook = useSqlarkDatabase({ name: 'vue-fail', mode: 'unknown-mode' as any });
|
|
await flushMounted();
|
|
|
|
expect(hook.db.value).toBeNull();
|
|
expect(hook.ready.value).toBe(false);
|
|
expect(hook.error.value).toBeInstanceOf(Error);
|
|
});
|
|
|
|
test('组件卸载时关闭数据库实例(v0.7.3)', async () => {
|
|
const hook = useSqlarkDatabase({ name: 'vue-unmount', mode: 'memory' });
|
|
await flushMounted();
|
|
expect(hook.db.value).not.toBeNull();
|
|
const db = hook.db.value!;
|
|
// 实例打开中:isReady
|
|
expect(db.isReady()).toBe(true);
|
|
|
|
await flushUnmounted();
|
|
// close 后实例不可再查询
|
|
await expect(db.query('SELECT 1')).rejects.toMatchObject({ code: 'DB_NOT_READY' });
|
|
});
|
|
|
|
test('卸载时实例为 null 不抛错(初始化失败场景)', async () => {
|
|
const hook = useSqlarkDatabase({ name: 'vue-unmount-fail', mode: 'unknown-mode' as any });
|
|
await flushMounted();
|
|
await expect(flushUnmounted()).resolves.toBeUndefined();
|
|
expect(hook.db.value).toBeNull();
|
|
});
|
|
});
|